@zucker-framework/ext-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1781 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/util.ts
5
+ function delay(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ __name(delay, "delay");
9
+ function clamp(value, min, max) {
10
+ return Math.min(Math.max(value, min), max);
11
+ }
12
+ __name(clamp, "clamp");
13
+ function normalizeError(error) {
14
+ return error instanceof Error ? error.message : String(error);
15
+ }
16
+ __name(normalizeError, "normalizeError");
17
+ function storageGet(keys) {
18
+ return new Promise((resolve) => chrome.storage.local.get(keys, resolve));
19
+ }
20
+ __name(storageGet, "storageGet");
21
+ function storageSet(value) {
22
+ return new Promise((resolve) => chrome.storage.local.set(value, () => resolve()));
23
+ }
24
+ __name(storageSet, "storageSet");
25
+ function storageRemove(keys) {
26
+ return new Promise((resolve) => chrome.storage.local.remove(keys, () => resolve()));
27
+ }
28
+ __name(storageRemove, "storageRemove");
29
+
30
+ // src/naming.ts
31
+ function sanitize(value) {
32
+ return String(value || "").replace(/[\\/:*?"<>|#&=%]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
33
+ }
34
+ __name(sanitize, "sanitize");
35
+ function hostOf(url) {
36
+ try {
37
+ return new URL(url).host;
38
+ } catch {
39
+ return String(url || "");
40
+ }
41
+ }
42
+ __name(hostOf, "hostOf");
43
+ function normalizeUrls(urls) {
44
+ const out = [];
45
+ const seen = /* @__PURE__ */ new Set();
46
+ for (const raw of urls || []) {
47
+ const value = String(raw || "").trim();
48
+ if (!/^https?:\/\//i.test(value)) {
49
+ continue;
50
+ }
51
+ let key = value;
52
+ try {
53
+ const parsed = new URL(value);
54
+ parsed.hash = "";
55
+ key = parsed.toString();
56
+ } catch {
57
+ continue;
58
+ }
59
+ if (!seen.has(key)) {
60
+ seen.add(key);
61
+ out.push(key);
62
+ }
63
+ if (out.length >= 500) {
64
+ break;
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+ __name(normalizeUrls, "normalizeUrls");
70
+ function sessionStamp() {
71
+ const d = /* @__PURE__ */ new Date();
72
+ const p = /* @__PURE__ */ __name((n) => String(n).padStart(2, "0"), "p");
73
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}`;
74
+ }
75
+ __name(sessionStamp, "sessionStamp");
76
+ function fileStamp() {
77
+ const d = /* @__PURE__ */ new Date();
78
+ const p = /* @__PURE__ */ __name((n) => String(n).padStart(2, "0"), "p");
79
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
80
+ }
81
+ __name(fileStamp, "fileStamp");
82
+ function todayString() {
83
+ const d = /* @__PURE__ */ new Date();
84
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
85
+ }
86
+ __name(todayString, "todayString");
87
+ function folderNameFor(urls) {
88
+ const hosts = /* @__PURE__ */ new Set();
89
+ for (const url of urls) {
90
+ try {
91
+ hosts.add(new URL(url).hostname.replace(/^www\./, ""));
92
+ } catch {
93
+ }
94
+ }
95
+ const stamp = sessionStamp();
96
+ if (hosts.size === 1) {
97
+ const host = sanitize([
98
+ ...hosts
99
+ ][0]).slice(0, 60);
100
+ return host ? `${host}-${stamp}` : stamp;
101
+ }
102
+ return stamp;
103
+ }
104
+ __name(folderNameFor, "folderNameFor");
105
+ function urlBaseName(url, maxLength, includeQuery) {
106
+ let base = "page";
107
+ try {
108
+ const parsed = new URL(url);
109
+ const host = parsed.hostname.replace(/^www\./, "");
110
+ let rest = (parsed.pathname + (includeQuery ? parsed.search : "")).replace(/\/+$/, "");
111
+ try {
112
+ rest = decodeURIComponent(rest);
113
+ } catch {
114
+ }
115
+ base = host + rest;
116
+ } catch {
117
+ base = url;
118
+ }
119
+ return sanitize(base).slice(0, maxLength) || "page";
120
+ }
121
+ __name(urlBaseName, "urlBaseName");
122
+ function buildBatchFileName(folder, item, extension) {
123
+ const base = urlBaseName(item.url, 120, true);
124
+ const index = String(item.index).padStart(3, "0");
125
+ return `${folder}/${index}_${base}.${extension}`;
126
+ }
127
+ __name(buildBatchFileName, "buildBatchFileName");
128
+ function buildQuickFileName(brand, quickFolder, pageUrl, extension) {
129
+ const base = urlBaseName(pageUrl, 110, false);
130
+ return `${brand}/${quickFolder}/${base}-${fileStamp()}.${extension}`;
131
+ }
132
+ __name(buildQuickFileName, "buildQuickFileName");
133
+
134
+ // src/licensing.ts
135
+ var CACHE_TTL_MS = 12 * 60 * 60 * 1e3;
136
+ function unwrap(payload) {
137
+ const obj = payload;
138
+ return obj && typeof obj === "object" && "result" in obj ? obj.result : obj || {};
139
+ }
140
+ __name(unwrap, "unwrap");
141
+ function createLicensing(product, brand, config) {
142
+ const TOKEN_KEY = `${product}_license_token`;
143
+ const ENTITLEMENT_KEY = `${product}_entitlement`;
144
+ const DEVICE_KEY = `${product}_device_id`;
145
+ async function getDeviceId() {
146
+ const data = await storageGet(DEVICE_KEY);
147
+ if (data[DEVICE_KEY]) {
148
+ return data[DEVICE_KEY];
149
+ }
150
+ const bytes = new Uint8Array(16);
151
+ crypto.getRandomValues(bytes);
152
+ const id = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
153
+ await storageSet({
154
+ [DEVICE_KEY]: id
155
+ });
156
+ return id;
157
+ }
158
+ __name(getDeviceId, "getDeviceId");
159
+ async function activate(code) {
160
+ const deviceId = await getDeviceId();
161
+ let response;
162
+ try {
163
+ const platform = navigator.userAgentData?.platform || "Chrome";
164
+ response = await fetch(`${config.apiBase}/extension/licenses/activate`, {
165
+ method: "POST",
166
+ headers: {
167
+ "Content-Type": "application/json"
168
+ },
169
+ body: JSON.stringify({
170
+ code: String(code || "").trim().toUpperCase().replace(/[^A-Z0-9]/g, ""),
171
+ deviceId,
172
+ deviceName: `${brand} \xB7 ${platform}`
173
+ })
174
+ });
175
+ } catch {
176
+ throw new Error("License service unreachable. Check your connection and retry.");
177
+ }
178
+ const payload = await response.json().catch(() => ({}));
179
+ if (!response.ok) {
180
+ if (response.status === 403) {
181
+ throw new Error("Invalid code, or its device limit is reached.");
182
+ }
183
+ throw new Error("Activation failed. Please retry later.");
184
+ }
185
+ const data = unwrap(payload);
186
+ await storageSet({
187
+ [TOKEN_KEY]: data.token || "",
188
+ [ENTITLEMENT_KEY]: {
189
+ ...data.entitlement || {},
190
+ checkedAt: Date.now()
191
+ }
192
+ });
193
+ return data.entitlement;
194
+ }
195
+ __name(activate, "activate");
196
+ function normalize(value) {
197
+ const v = value;
198
+ if (!v) {
199
+ return null;
200
+ }
201
+ const expired = v.expiresAt && new Date(v.expiresAt).getTime() <= Date.now();
202
+ return expired ? null : {
203
+ plan: v.plan || "pro",
204
+ expiresAt: v.expiresAt || ""
205
+ };
206
+ }
207
+ __name(normalize, "normalize");
208
+ async function getEntitlement() {
209
+ const data = await storageGet([
210
+ TOKEN_KEY,
211
+ ENTITLEMENT_KEY
212
+ ]);
213
+ const token = data[TOKEN_KEY];
214
+ const cached = data[ENTITLEMENT_KEY];
215
+ if (!token) {
216
+ return null;
217
+ }
218
+ if (cached?.checkedAt && Date.now() - cached.checkedAt < CACHE_TTL_MS) {
219
+ return normalize(cached);
220
+ }
221
+ let response;
222
+ try {
223
+ response = await fetch(`${config.apiBase}/extension/licenses/me`, {
224
+ headers: {
225
+ Authorization: `Bearer ${token}`
226
+ }
227
+ });
228
+ } catch {
229
+ return cached ? normalize(cached) : null;
230
+ }
231
+ if (!response.ok) {
232
+ if (response.status === 401 || response.status === 403) {
233
+ await clear();
234
+ return null;
235
+ }
236
+ return cached ? normalize(cached) : null;
237
+ }
238
+ const payload = await response.json().catch(() => ({}));
239
+ const entitlement = unwrap(payload)?.entitlement;
240
+ await storageSet({
241
+ [ENTITLEMENT_KEY]: {
242
+ ...entitlement || {},
243
+ checkedAt: Date.now()
244
+ }
245
+ });
246
+ return normalize(entitlement);
247
+ }
248
+ __name(getEntitlement, "getEntitlement");
249
+ async function isPro() {
250
+ return Boolean(await getEntitlement());
251
+ }
252
+ __name(isPro, "isPro");
253
+ async function clear() {
254
+ await storageRemove([
255
+ TOKEN_KEY,
256
+ ENTITLEMENT_KEY
257
+ ]);
258
+ }
259
+ __name(clear, "clear");
260
+ return {
261
+ activate,
262
+ getEntitlement,
263
+ isPro,
264
+ clear,
265
+ purchaseUrl: config.purchaseUrl
266
+ };
267
+ }
268
+ __name(createLicensing, "createLicensing");
269
+
270
+ // src/download.ts
271
+ function createDownloader(justification) {
272
+ const pendingDownloadNames = /* @__PURE__ */ new Map();
273
+ let offscreenSetup = null;
274
+ chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
275
+ const fileName = pendingDownloadNames.get(item.url) || pendingDownloadNames.get(item.finalUrl || "");
276
+ if (!fileName) {
277
+ return;
278
+ }
279
+ suggest({
280
+ filename: fileName,
281
+ conflictAction: "uniquify"
282
+ });
283
+ return true;
284
+ });
285
+ async function ensureOffscreen() {
286
+ if (await chrome.offscreen.hasDocument()) {
287
+ return;
288
+ }
289
+ if (!offscreenSetup) {
290
+ offscreenSetup = chrome.offscreen.createDocument({
291
+ url: "offscreen.html",
292
+ reasons: [
293
+ chrome.offscreen.Reason.BLOBS
294
+ ],
295
+ justification
296
+ }).catch((error) => {
297
+ if (!String(error?.message || "").includes("Only a single")) {
298
+ throw error;
299
+ }
300
+ }).finally(() => {
301
+ offscreenSetup = null;
302
+ });
303
+ }
304
+ return offscreenSetup;
305
+ }
306
+ __name(ensureOffscreen, "ensureOffscreen");
307
+ async function downloadViaBlob(base64, mime, filename) {
308
+ await ensureOffscreen();
309
+ const created = await chrome.runtime.sendMessage({
310
+ target: "offscreen",
311
+ type: "CREATE_BLOB_URL",
312
+ base64,
313
+ mime
314
+ });
315
+ if (!created?.ok || !created.url) {
316
+ throw new Error(created?.error || "Failed to prepare download");
317
+ }
318
+ const url = created.url;
319
+ pendingDownloadNames.set(url, filename);
320
+ const cleanup = /* @__PURE__ */ __name(() => {
321
+ pendingDownloadNames.delete(url);
322
+ chrome.runtime.sendMessage({
323
+ target: "offscreen",
324
+ type: "REVOKE_BLOB_URL",
325
+ url
326
+ }).catch(() => {
327
+ });
328
+ }, "cleanup");
329
+ try {
330
+ const downloadId = await new Promise((resolve, reject) => {
331
+ chrome.downloads.download({
332
+ url,
333
+ filename,
334
+ conflictAction: "uniquify",
335
+ saveAs: false
336
+ }, (id) => {
337
+ const error = chrome.runtime.lastError;
338
+ error ? reject(new Error(error.message)) : resolve(id);
339
+ });
340
+ });
341
+ setTimeout(cleanup, 6e4);
342
+ return downloadId;
343
+ } catch (error) {
344
+ cleanup();
345
+ throw error;
346
+ }
347
+ }
348
+ __name(downloadViaBlob, "downloadViaBlob");
349
+ return {
350
+ downloadViaBlob
351
+ };
352
+ }
353
+ __name(createDownloader, "createDownloader");
354
+
355
+ // src/session.ts
356
+ function debuggerAttach(target, version) {
357
+ return new Promise((resolve, reject) => {
358
+ chrome.debugger.attach(target, version, () => {
359
+ const error = chrome.runtime.lastError;
360
+ error ? reject(new Error(error.message)) : resolve();
361
+ });
362
+ });
363
+ }
364
+ __name(debuggerAttach, "debuggerAttach");
365
+ function debuggerDetach(target) {
366
+ return new Promise((resolve, reject) => {
367
+ chrome.debugger.detach(target, () => {
368
+ const error = chrome.runtime.lastError;
369
+ error ? reject(new Error(error.message)) : resolve();
370
+ });
371
+ });
372
+ }
373
+ __name(debuggerDetach, "debuggerDetach");
374
+ function sendCommand(target, method, params) {
375
+ return new Promise((resolve, reject) => {
376
+ chrome.debugger.sendCommand(target, method, params || {}, (result) => {
377
+ const error = chrome.runtime.lastError;
378
+ error ? reject(new Error(error.message)) : resolve(result || {});
379
+ });
380
+ });
381
+ }
382
+ __name(sendCommand, "sendCommand");
383
+ function waitForLoad(tabId, timeoutMs) {
384
+ return new Promise((resolve, reject) => {
385
+ let settled = false;
386
+ const finish = /* @__PURE__ */ __name((fn, value) => {
387
+ if (!settled) {
388
+ settled = true;
389
+ chrome.tabs.onUpdated.removeListener(listener);
390
+ clearTimeout(timer);
391
+ fn(value);
392
+ }
393
+ }, "finish");
394
+ const listener = /* @__PURE__ */ __name((updatedTabId, changeInfo) => {
395
+ if (updatedTabId === tabId && changeInfo.status === "complete") {
396
+ finish(resolve);
397
+ }
398
+ }, "listener");
399
+ const timer = setTimeout(() => finish(reject, new Error("Page load timeout")), timeoutMs);
400
+ chrome.tabs.onUpdated.addListener(listener);
401
+ chrome.tabs.get(tabId).then((tab) => {
402
+ if (tab.status === "complete") {
403
+ finish(resolve);
404
+ }
405
+ }).catch(() => {
406
+ });
407
+ });
408
+ }
409
+ __name(waitForLoad, "waitForLoad");
410
+ function challengeError(name) {
411
+ const error = new Error(`Blocked by ${name} verification`);
412
+ error.isChallenge = true;
413
+ return error;
414
+ }
415
+ __name(challengeError, "challengeError");
416
+ function isChallengeError(error) {
417
+ return Boolean(error?.isChallenge);
418
+ }
419
+ __name(isChallengeError, "isChallengeError");
420
+ var CHALLENGE_TITLE_RE = /just a moment|checking your browser|attention required|verifying|security checkpoint|正在验证|安全检查点|verify you are human|are you a robot|请完成验证|人机验证|captcha/;
421
+ async function probePage(target) {
422
+ const r = await sendCommand(target, "Runtime.evaluate", {
423
+ returnByValue: true,
424
+ expression: `(function(){
425
+ var uri=document.documentURI||"";
426
+ if(uri.indexOf("chrome-error://")===0) return {error:"Page failed to load (DNS or network error)",challenge:""};
427
+ var t=(document.title||"").toLowerCase();
428
+ var b=(document.body?document.body.innerText:"").slice(0,500).toLowerCase();
429
+ var u=(location.href||"").toLowerCase();
430
+ var s=t+" "+b+" "+u;
431
+ var c="";
432
+ if(/just a moment|checking your browser|attention required|cf-browser-verification|challenges\\.cloudflare/.test(s)) c="Cloudflare";
433
+ 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";
434
+ 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";
435
+ return {error:"",challenge:c};
436
+ })()`
437
+ });
438
+ const value = r.result?.value;
439
+ return value || {
440
+ error: "",
441
+ challenge: ""
442
+ };
443
+ }
444
+ __name(probePage, "probePage");
445
+ async function waitForVerification(tabId, shouldStop, timeoutMs = 18e4) {
446
+ const start = Date.now();
447
+ while (Date.now() - start < timeoutMs) {
448
+ if (shouldStop()) {
449
+ return false;
450
+ }
451
+ await delay(2500);
452
+ const tab = await chrome.tabs.get(tabId).catch(() => null);
453
+ if (!tab) {
454
+ return false;
455
+ }
456
+ const title = (tab.title || "").toLowerCase();
457
+ const url = (tab.url || "").toLowerCase();
458
+ if (url.startsWith("chrome-error")) {
459
+ continue;
460
+ }
461
+ if (title && !CHALLENGE_TITLE_RE.test(title)) {
462
+ return true;
463
+ }
464
+ }
465
+ return false;
466
+ }
467
+ __name(waitForVerification, "waitForVerification");
468
+ async function focusTab(tabId) {
469
+ await chrome.tabs.update(tabId, {
470
+ active: true
471
+ }).catch(() => {
472
+ });
473
+ try {
474
+ const tab = await chrome.tabs.get(tabId);
475
+ if (tab.windowId != null) {
476
+ await chrome.windows.update(tab.windowId, {
477
+ focused: true
478
+ });
479
+ }
480
+ } catch {
481
+ }
482
+ }
483
+ __name(focusTab, "focusTab");
484
+ function autoScroll(target) {
485
+ return sendCommand(target, "Runtime.evaluate", {
486
+ awaitPromise: true,
487
+ expression: `new Promise(function(resolve){
488
+ var distance=600, scrolled=0, guard=0;
489
+ var maxHeight=Math.min(document.body?document.body.scrollHeight:0, 20000);
490
+ var timer=setInterval(function(){
491
+ window.scrollBy(0, distance);
492
+ scrolled+=distance; guard+=1;
493
+ var reached=scrolled>=Math.max(maxHeight, document.body?document.body.scrollHeight:0);
494
+ if(reached || guard>60){
495
+ clearInterval(timer);
496
+ setTimeout(function(){ window.scrollTo(0,0); setTimeout(resolve, 400); }, 300);
497
+ }
498
+ }, 120);
499
+ })`
500
+ }).catch(() => void 0);
501
+ }
502
+ __name(autoScroll, "autoScroll");
503
+ function revealAnimations(target, reversible = false) {
504
+ return sendCommand(target, "Runtime.evaluate", {
505
+ expression: `(function(){
506
+ try{
507
+ var reversible=${reversible ? "true" : "false"};
508
+ var state=reversible?{style:null,aos:[],inline:[]}:null;
509
+ var css=document.createElement('style');
510
+ css.textContent=[
511
+ '[data-aos]{opacity:1!important;transform:none!important;transition:none!important;}',
512
+ '.wow{visibility:visible!important;opacity:1!important;animation-name:none!important;}',
513
+ '.animate__animated,.animated{opacity:1!important;}',
514
+ '[data-scroll]{opacity:1!important;transform:none!important;}',
515
+ '.reveal,[data-sr],[data-sr-id],.sr-reveal{opacity:1!important;transform:none!important;visibility:visible!important;}'
516
+ ].join('');
517
+ (document.head||document.documentElement).appendChild(css);
518
+ if(state) state.style=css;
519
+ document.querySelectorAll('[data-aos]').forEach(function(el){
520
+ if(state&&!el.classList.contains('aos-animate')) state.aos.push(el);
521
+ el.classList.add('aos-animate');
522
+ });
523
+ // \u901A\u7528\u515C\u5E95\uFF1A\u88AB\u52A8\u753B\u85CF\u8D77\u6765\u7684\u5185\u5BB9\u5757\uFF08opacity\u22480 \u4E14\u5E26 transition/animation\uFF09\u5F3A\u5236\u663E\u793A\u3002
524
+ // \u53EA\u5904\u7406 static/relative\uFF08\u666E\u901A\u5185\u5BB9\u6D41\uFF09\uFF0C\u8DF3\u8FC7 fixed/absolute\uFF08\u591A\u4E3A\u5F39\u7A97/\u83DC\u5355/\u8F6E\u64AD\uFF09\uFF0C\u907F\u514D\u8BEF\u4F24
525
+ var all=document.body?document.body.querySelectorAll('*'):[];
526
+ for(var i=0;i<all.length;i++){
527
+ var el=all[i], s=getComputedStyle(el);
528
+ if(parseFloat(s.opacity)<0.05 && (s.transitionDuration!=='0s'||s.animationName!=='none')){
529
+ var p=s.position;
530
+ if(p==='static'||p==='relative'){
531
+ if(state) state.inline.push({
532
+ el:el,
533
+ opacity:el.style.getPropertyValue('opacity'),
534
+ opacityPriority:el.style.getPropertyPriority('opacity'),
535
+ transform:el.style.getPropertyValue('transform'),
536
+ transformPriority:el.style.getPropertyPriority('transform')
537
+ });
538
+ el.style.setProperty('opacity','1','important');
539
+ el.style.setProperty('transform','none','important');
540
+ }
541
+ }
542
+ }
543
+ if(state) window.__zuckerExtRevealState=state;
544
+ }catch(e){}
545
+ })()`
546
+ }).catch(() => void 0);
547
+ }
548
+ __name(revealAnimations, "revealAnimations");
549
+ async function getScrollPosition(target) {
550
+ const result = await sendCommand(target, "Runtime.evaluate", {
551
+ returnByValue: true,
552
+ expression: `({ x: window.scrollX || 0, y: window.scrollY || 0 })`
553
+ });
554
+ const value = result.result?.value;
555
+ return value || {
556
+ x: 0,
557
+ y: 0
558
+ };
559
+ }
560
+ __name(getScrollPosition, "getScrollPosition");
561
+ function restorePageAfterQuick(target, position) {
562
+ const x = Number(position?.x) || 0;
563
+ const y = Number(position?.y) || 0;
564
+ return sendCommand(target, "Runtime.evaluate", {
565
+ expression: `(function(){
566
+ try{
567
+ var state=window.__zuckerExtRevealState;
568
+ if(state){
569
+ if(state.style&&state.style.remove) state.style.remove();
570
+ (state.aos||[]).forEach(function(el){ el.classList.remove('aos-animate'); });
571
+ (state.inline||[]).forEach(function(saved){
572
+ if(saved.opacity) saved.el.style.setProperty('opacity',saved.opacity,saved.opacityPriority||'');
573
+ else saved.el.style.removeProperty('opacity');
574
+ if(saved.transform) saved.el.style.setProperty('transform',saved.transform,saved.transformPriority||'');
575
+ else saved.el.style.removeProperty('transform');
576
+ });
577
+ delete window.__zuckerExtRevealState;
578
+ }
579
+ }catch(e){}
580
+ window.scrollTo(${x}, ${y});
581
+ })()`
582
+ });
583
+ }
584
+ __name(restorePageAfterQuick, "restorePageAfterQuick");
585
+
586
+ // src/engine.ts
587
+ var LOAD_TIMEOUT_MS = 3e4;
588
+ var SAME_HOST_MIN_GAP_MS = 1800;
589
+ var DEFAULT_TEXTS = {
590
+ alreadyRunning: "Another capture is already running",
591
+ cannotQuick: "This page cannot be captured. Open an http(s) website first.",
592
+ challengeBeforeQuick: /* @__PURE__ */ __name((challenge) => `Complete the ${challenge} verification before capturing this page.`, "challengeBeforeQuick"),
593
+ quickFolder: "Quick Captures"
594
+ };
595
+ function initExtension(config) {
596
+ const texts = {
597
+ ...DEFAULT_TEXTS,
598
+ ...config.texts || {}
599
+ };
600
+ const freeDailyLimit = config.freeDailyLimit ?? 30;
601
+ const USAGE_KEY = `${config.product}_free_usage`;
602
+ const BATCH_KEY = `${config.product}_batch_state`;
603
+ const prefix = config.messagePrefix;
604
+ const licensing = createLicensing(config.product, config.brand, config.licensing);
605
+ const downloader = createDownloader(`Convert large ${config.brand} output into a downloadable blob URL.`);
606
+ let batch = idleBatch();
607
+ let restorePromise = restoreBatch();
608
+ let quickRunning = false;
609
+ chrome.runtime.onInstalled.addListener(() => {
610
+ const previousRestore = restorePromise;
611
+ restorePromise = (async () => {
612
+ await previousRestore.catch(() => {
613
+ });
614
+ batch = idleBatch();
615
+ await storageRemove(BATCH_KEY);
616
+ setBadge("");
617
+ })();
618
+ });
619
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
620
+ if (message?.target === "offscreen") {
621
+ return false;
622
+ }
623
+ handleMessage(message).then(sendResponse).catch((error) => sendResponse({
624
+ ok: false,
625
+ error: normalizeError(error)
626
+ }));
627
+ return true;
628
+ });
629
+ function idleBatch() {
630
+ return {
631
+ running: false,
632
+ cancelRequested: false,
633
+ startedAt: null,
634
+ folder: "",
635
+ total: 0,
636
+ done: 0,
637
+ success: 0,
638
+ failed: 0,
639
+ current: "",
640
+ freeLimited: false,
641
+ needVerify: null,
642
+ skipVerify: false,
643
+ settings: {
644
+ ...config.defaultSettings
645
+ },
646
+ pro: false,
647
+ items: []
648
+ };
649
+ }
650
+ __name(idleBatch, "idleBatch");
651
+ async function handleMessage(message) {
652
+ await restorePromise;
653
+ switch (message.type) {
654
+ case `${prefix}_START`:
655
+ return startBatch(message.urls, message.settings);
656
+ // 两个后缀都接受:CAPTURE_CURRENT(截图类)/ CONVERT_CURRENT(转换类),兼容既有 popup
657
+ case `${prefix}_CAPTURE_CURRENT`:
658
+ case `${prefix}_CONVERT_CURRENT`:
659
+ return quickCapture(message.tabId, message.settings);
660
+ case `${prefix}_CANCEL`:
661
+ return cancelBatch();
662
+ case `${prefix}_CLEAR_BATCH`:
663
+ return clearBatch();
664
+ case `${prefix}_SKIP_VERIFY`:
665
+ batch.skipVerify = true;
666
+ await persistBatch();
667
+ return {
668
+ ok: true
669
+ };
670
+ case `${prefix}_STATE`:
671
+ return {
672
+ ok: true,
673
+ batch: publicBatch(),
674
+ quota: await getQuota()
675
+ };
676
+ case `${prefix}_ACTIVATE`:
677
+ await licensing.activate(message.code);
678
+ return {
679
+ ok: true,
680
+ quota: await getQuota()
681
+ };
682
+ case `${prefix}_CLEAR_LICENSE`:
683
+ await licensing.clear();
684
+ return {
685
+ ok: true,
686
+ quota: await getQuota()
687
+ };
688
+ default:
689
+ return {
690
+ ok: false,
691
+ error: "Unknown message"
692
+ };
693
+ }
694
+ }
695
+ __name(handleMessage, "handleMessage");
696
+ async function getQuota() {
697
+ const entitlement = await licensing.getEntitlement().catch(() => null);
698
+ if (entitlement) {
699
+ return {
700
+ pro: true,
701
+ expiresAt: entitlement.expiresAt,
702
+ limit: null,
703
+ used: 0,
704
+ remaining: 999999
705
+ };
706
+ }
707
+ const data = await storageGet(USAGE_KEY);
708
+ const usage = data[USAGE_KEY];
709
+ const today = todayString();
710
+ const used = usage && usage.date === today ? usage.used || 0 : 0;
711
+ return {
712
+ pro: false,
713
+ limit: freeDailyLimit,
714
+ used,
715
+ remaining: Math.max(0, freeDailyLimit - used)
716
+ };
717
+ }
718
+ __name(getQuota, "getQuota");
719
+ async function recordUsage(count) {
720
+ const data = await storageGet(USAGE_KEY);
721
+ const today = todayString();
722
+ const usage = data[USAGE_KEY];
723
+ const used = usage && usage.date === today ? usage.used || 0 : 0;
724
+ await storageSet({
725
+ [USAGE_KEY]: {
726
+ date: today,
727
+ used: used + count
728
+ }
729
+ });
730
+ }
731
+ __name(recordUsage, "recordUsage");
732
+ async function startBatch(urls, settings) {
733
+ if (batch.running || quickRunning) {
734
+ return {
735
+ ok: false,
736
+ error: texts.alreadyRunning
737
+ };
738
+ }
739
+ const normalized = normalizeUrls(urls);
740
+ if (!normalized.length) {
741
+ return {
742
+ ok: false,
743
+ error: "No valid http(s) URLs"
744
+ };
745
+ }
746
+ const quota = await getQuota();
747
+ if (!quota.pro && quota.remaining <= 0) {
748
+ return {
749
+ ok: false,
750
+ error: `Daily free limit reached (${quota.limit} pages/day). Go Pro for unlimited pages.`
751
+ };
752
+ }
753
+ const freeLimited = !quota.pro && normalized.length > quota.remaining;
754
+ const accepted = quota.pro ? normalized : normalized.slice(0, quota.remaining);
755
+ const options = {
756
+ ...config.defaultSettings,
757
+ ...settings || {}
758
+ };
759
+ batch = {
760
+ ...idleBatch(),
761
+ running: true,
762
+ startedAt: Date.now(),
763
+ folder: `${config.brand}/${folderNameFor(accepted)}`,
764
+ total: accepted.length,
765
+ freeLimited,
766
+ settings: options,
767
+ pro: quota.pro,
768
+ items: accepted.map((url, index) => ({
769
+ url,
770
+ index: index + 1,
771
+ status: "pending",
772
+ fileName: "",
773
+ error: "",
774
+ warning: "",
775
+ usageRecorded: false
776
+ }))
777
+ };
778
+ await persistBatch();
779
+ runBatch(options).catch((error) => {
780
+ batch.running = false;
781
+ batch.current = normalizeError(error);
782
+ persistBatch();
783
+ });
784
+ return {
785
+ ok: true,
786
+ batch: publicBatch(),
787
+ quota: await getQuota()
788
+ };
789
+ }
790
+ __name(startBatch, "startBatch");
791
+ async function quickCapture(tabId, settings) {
792
+ if (batch.running || quickRunning) {
793
+ return {
794
+ ok: false,
795
+ error: texts.alreadyRunning
796
+ };
797
+ }
798
+ if (!Number.isInteger(tabId)) {
799
+ return {
800
+ ok: false,
801
+ error: "No active web page"
802
+ };
803
+ }
804
+ const tab = await chrome.tabs.get(tabId).catch(() => null);
805
+ if (!tab?.url || !/^https?:\/\//i.test(tab.url)) {
806
+ return {
807
+ ok: false,
808
+ error: texts.cannotQuick
809
+ };
810
+ }
811
+ const quota = await getQuota();
812
+ if (!quota.pro && quota.remaining <= 0) {
813
+ return {
814
+ ok: false,
815
+ error: `Daily free limit reached (${quota.limit} pages/day). Go Pro for unlimited pages.`
816
+ };
817
+ }
818
+ quickRunning = true;
819
+ setBadge("1");
820
+ try {
821
+ const options = {
822
+ ...config.defaultSettings,
823
+ ...settings || {}
824
+ };
825
+ const result = await captureExistingTab(tabId, tab.url, options);
826
+ if (!quota.pro) {
827
+ await recordUsage(1);
828
+ }
829
+ setBadge("OK");
830
+ setTimeout(() => setBadge(""), 4e3);
831
+ return {
832
+ ok: true,
833
+ result,
834
+ quota: await getQuota()
835
+ };
836
+ } catch (error) {
837
+ setBadge("ERR");
838
+ setTimeout(() => setBadge(""), 4e3);
839
+ return {
840
+ ok: false,
841
+ error: normalizeError(error),
842
+ quota: await getQuota()
843
+ };
844
+ } finally {
845
+ quickRunning = false;
846
+ }
847
+ }
848
+ __name(quickCapture, "quickCapture");
849
+ async function captureExistingTab(tabId, pageUrl, options) {
850
+ const target = {
851
+ tabId
852
+ };
853
+ let attached = false;
854
+ let scrollPosition = {
855
+ x: 0,
856
+ y: 0
857
+ };
858
+ try {
859
+ await debuggerAttach(target, "1.3");
860
+ attached = true;
861
+ await sendCommand(target, "Page.enable");
862
+ await sendCommand(target, "Runtime.enable");
863
+ const probe = await probePage(target);
864
+ if (probe.error) {
865
+ throw new Error(probe.error);
866
+ }
867
+ if (probe.challenge) {
868
+ throw new Error(texts.challengeBeforeQuick(probe.challenge));
869
+ }
870
+ scrollPosition = await getScrollPosition(target);
871
+ await delay(clamp(Number(options.delayMs) || Number(config.defaultSettings.delayMs) || 1500, 0, 1e4));
872
+ await autoScroll(target);
873
+ await revealAnimations(target, true);
874
+ await delay(400);
875
+ const output = await config.capture(makeContext("quick", tabId, pageUrl, options, target));
876
+ const fileName = buildQuickFileName(config.brand, texts.quickFolder, pageUrl, output.extension);
877
+ await downloader.downloadViaBlob(output.base64, output.mime, fileName);
878
+ return {
879
+ fileName,
880
+ warning: output.warning || ""
881
+ };
882
+ } finally {
883
+ if (attached) {
884
+ await restorePageAfterQuick(target, scrollPosition).catch(() => {
885
+ });
886
+ await debuggerDetach(target).catch(() => {
887
+ });
888
+ }
889
+ }
890
+ }
891
+ __name(captureExistingTab, "captureExistingTab");
892
+ async function cancelBatch() {
893
+ if (batch.running) {
894
+ batch.cancelRequested = true;
895
+ batch.current = "Stopping\u2026";
896
+ await persistBatch();
897
+ }
898
+ return {
899
+ ok: true,
900
+ batch: publicBatch()
901
+ };
902
+ }
903
+ __name(cancelBatch, "cancelBatch");
904
+ async function clearBatch() {
905
+ if (batch.running) {
906
+ return {
907
+ ok: false,
908
+ error: "Stop the running batch before clearing it"
909
+ };
910
+ }
911
+ batch = idleBatch();
912
+ await storageRemove(BATCH_KEY);
913
+ setBadge("");
914
+ return {
915
+ ok: true,
916
+ batch: publicBatch()
917
+ };
918
+ }
919
+ __name(clearBatch, "clearBatch");
920
+ async function runBatch(options) {
921
+ const lastHostAt = {};
922
+ for (const item of batch.items) {
923
+ if (item.status !== "pending") {
924
+ continue;
925
+ }
926
+ if (batch.cancelRequested) {
927
+ break;
928
+ }
929
+ const host = hostOf(item.url);
930
+ const since = Date.now() - (lastHostAt[host] || 0);
931
+ if (lastHostAt[host] && since < SAME_HOST_MIN_GAP_MS) {
932
+ batch.current = "Pacing to avoid rate-limits\u2026";
933
+ await delay(SAME_HOST_MIN_GAP_MS - since);
934
+ if (batch.cancelRequested) {
935
+ break;
936
+ }
937
+ }
938
+ lastHostAt[host] = Date.now();
939
+ item.status = "running";
940
+ batch.current = item.url;
941
+ setBadge(`${batch.done + 1}/${batch.total}`);
942
+ await persistBatch();
943
+ let shouldCharge = false;
944
+ try {
945
+ const result = await captureUrl(item, options);
946
+ item.fileName = result.fileName;
947
+ item.warning = result.warning;
948
+ item.status = "success";
949
+ batch.success += 1;
950
+ if (!batch.pro && !item.usageRecorded) {
951
+ item.usageRecorded = true;
952
+ shouldCharge = true;
953
+ }
954
+ } catch (error) {
955
+ item.status = "failed";
956
+ item.error = normalizeError(error);
957
+ batch.failed += 1;
958
+ }
959
+ batch.done = batch.success + batch.failed;
960
+ await persistBatch();
961
+ if (shouldCharge) {
962
+ await recordUsage(1);
963
+ }
964
+ }
965
+ batch.running = false;
966
+ batch.current = batch.cancelRequested ? "Stopped" : "Done";
967
+ batch.needVerify = null;
968
+ await persistBatch();
969
+ setBadge(batch.failed ? "ERR" : "OK");
970
+ setTimeout(() => setBadge(""), 4e3);
971
+ }
972
+ __name(runBatch, "runBatch");
973
+ async function captureUrl(item, options) {
974
+ const maxChallengeRetry = 2;
975
+ let lastError;
976
+ for (let attempt = 0; attempt <= maxChallengeRetry; attempt += 1) {
977
+ if (batch.cancelRequested) {
978
+ throw new Error("Cancelled");
979
+ }
980
+ try {
981
+ return await captureOnce(item, options, false);
982
+ } catch (error) {
983
+ lastError = error;
984
+ if (!isChallengeError(error)) {
985
+ throw error;
986
+ }
987
+ if (attempt < maxChallengeRetry) {
988
+ const backoff = 8e3 * (attempt + 1);
989
+ batch.current = `Rate-limited \u2014 cooling down ${backoff / 1e3}s (retry ${attempt + 1}/${maxChallengeRetry})`;
990
+ await delay(backoff);
991
+ }
992
+ }
993
+ }
994
+ if (isChallengeError(lastError) && !batch.cancelRequested) {
995
+ return await captureOnce(item, options, true);
996
+ }
997
+ throw lastError;
998
+ }
999
+ __name(captureUrl, "captureUrl");
1000
+ async function captureOnce(item, options, interactive) {
1001
+ const tab = await chrome.tabs.create({
1002
+ url: item.url,
1003
+ active: Boolean(interactive)
1004
+ });
1005
+ const tabId = tab.id;
1006
+ if (!tabId) {
1007
+ throw new Error("Could not open tab");
1008
+ }
1009
+ const target = {
1010
+ tabId
1011
+ };
1012
+ let attached = false;
1013
+ try {
1014
+ await waitForLoad(tabId, LOAD_TIMEOUT_MS);
1015
+ await delay(Number(options.delayMs) || 0);
1016
+ if (batch.cancelRequested) {
1017
+ throw new Error("Cancelled");
1018
+ }
1019
+ await debuggerAttach(target, "1.3");
1020
+ attached = true;
1021
+ await sendCommand(target, "Page.enable");
1022
+ await sendCommand(target, "Runtime.enable");
1023
+ const probe = await probePage(target);
1024
+ if (probe.error) {
1025
+ throw new Error(probe.error);
1026
+ }
1027
+ if (probe.challenge) {
1028
+ if (!interactive) {
1029
+ throw challengeError(probe.challenge);
1030
+ }
1031
+ await debuggerDetach(target).catch(() => {
1032
+ });
1033
+ attached = false;
1034
+ await focusTab(tabId);
1035
+ batch.needVerify = {
1036
+ url: item.url,
1037
+ challenge: probe.challenge
1038
+ };
1039
+ batch.current = `Complete ${probe.challenge} verification in the opened tab\u2026`;
1040
+ await persistBatch();
1041
+ batch.skipVerify = false;
1042
+ const passed = await waitForVerification(tabId, () => batch.cancelRequested || batch.skipVerify);
1043
+ batch.needVerify = null;
1044
+ batch.current = item.url;
1045
+ await persistBatch();
1046
+ if (!passed) {
1047
+ throw challengeError(`${probe.challenge} verification not completed`);
1048
+ }
1049
+ await delay(800);
1050
+ await debuggerAttach(target, "1.3");
1051
+ attached = true;
1052
+ await sendCommand(target, "Page.enable");
1053
+ await sendCommand(target, "Runtime.enable");
1054
+ }
1055
+ const ctx = makeContext("batch", tabId, item.url, options, target);
1056
+ if (config.beforeScroll) {
1057
+ await config.beforeScroll(ctx);
1058
+ }
1059
+ await autoScroll(target);
1060
+ await revealAnimations(target);
1061
+ await delay(400);
1062
+ const output = await config.capture(ctx);
1063
+ const fileName = buildBatchFileName(batch.folder, item, output.extension);
1064
+ await downloader.downloadViaBlob(output.base64, output.mime, fileName);
1065
+ return {
1066
+ fileName,
1067
+ warning: output.warning || ""
1068
+ };
1069
+ } finally {
1070
+ if (attached) {
1071
+ await debuggerDetach(target).catch(() => {
1072
+ });
1073
+ }
1074
+ await chrome.tabs.remove(tabId).catch(() => {
1075
+ });
1076
+ }
1077
+ }
1078
+ __name(captureOnce, "captureOnce");
1079
+ function makeContext(mode, tabId, url, options, target) {
1080
+ return {
1081
+ mode,
1082
+ tabId,
1083
+ url,
1084
+ options,
1085
+ send: /* @__PURE__ */ __name((method, params) => sendCommand(target, method, params), "send")
1086
+ };
1087
+ }
1088
+ __name(makeContext, "makeContext");
1089
+ function publicBatch() {
1090
+ return {
1091
+ running: batch.running,
1092
+ total: batch.total,
1093
+ done: batch.done,
1094
+ success: batch.success,
1095
+ failed: batch.failed,
1096
+ current: batch.current,
1097
+ folder: batch.folder,
1098
+ freeLimited: batch.freeLimited,
1099
+ needVerify: batch.needVerify,
1100
+ items: batch.items.map((item) => ({
1101
+ url: item.url,
1102
+ index: item.index,
1103
+ status: item.status,
1104
+ fileName: item.fileName,
1105
+ error: item.error,
1106
+ warning: item.warning
1107
+ }))
1108
+ };
1109
+ }
1110
+ __name(publicBatch, "publicBatch");
1111
+ async function persistBatch() {
1112
+ await storageSet({
1113
+ [BATCH_KEY]: batch
1114
+ });
1115
+ }
1116
+ __name(persistBatch, "persistBatch");
1117
+ async function restoreBatch() {
1118
+ const data = await storageGet(BATCH_KEY);
1119
+ const saved = data[BATCH_KEY];
1120
+ if (!saved?.items?.length) {
1121
+ return;
1122
+ }
1123
+ batch = {
1124
+ ...idleBatch(),
1125
+ ...saved
1126
+ };
1127
+ batch.settings = {
1128
+ ...config.defaultSettings,
1129
+ ...saved.settings || {}
1130
+ };
1131
+ batch.items = saved.items.map((item) => ({
1132
+ url: item.url || "",
1133
+ index: item.index || 0,
1134
+ status: item.status === "running" ? "pending" : item.status || "pending",
1135
+ fileName: item.fileName || "",
1136
+ error: item.error || "",
1137
+ warning: item.warning || "",
1138
+ usageRecorded: item.usageRecorded || false
1139
+ }));
1140
+ batch.done = batch.items.filter((item) => item.status === "success" || item.status === "failed").length;
1141
+ batch.success = batch.items.filter((item) => item.status === "success").length;
1142
+ batch.failed = batch.items.filter((item) => item.status === "failed").length;
1143
+ batch.needVerify = null;
1144
+ batch.skipVerify = false;
1145
+ if (saved.running && batch.items.some((item) => item.status === "pending")) {
1146
+ batch.running = true;
1147
+ batch.cancelRequested = false;
1148
+ batch.current = "Resuming interrupted batch\u2026";
1149
+ await persistBatch();
1150
+ runBatch(batch.settings).catch(async (error) => {
1151
+ batch.running = false;
1152
+ batch.current = normalizeError(error);
1153
+ await persistBatch();
1154
+ });
1155
+ } else if (saved.running) {
1156
+ batch.running = false;
1157
+ batch.current = "Done";
1158
+ await persistBatch();
1159
+ }
1160
+ }
1161
+ __name(restoreBatch, "restoreBatch");
1162
+ function setBadge(text) {
1163
+ chrome.action.setBadgeText({
1164
+ text
1165
+ });
1166
+ chrome.action.setBadgeBackgroundColor({
1167
+ color: config.badgeColor
1168
+ });
1169
+ }
1170
+ __name(setBadge, "setBadge");
1171
+ }
1172
+ __name(initExtension, "initExtension");
1173
+
1174
+ // src/popup.ts
1175
+ function initPopup(config) {
1176
+ const byId = /* @__PURE__ */ __name((id) => document.getElementById(id), "byId");
1177
+ const el = {
1178
+ urls: byId("urls"),
1179
+ urlCount: byId("urlCount"),
1180
+ addUrls: byId("addUrls"),
1181
+ scanSite: byId("scanSite"),
1182
+ queueSummary: byId("queueSummary"),
1183
+ queueList: byId("queueList"),
1184
+ selectAll: byId("selectAll"),
1185
+ selectNone: byId("selectNone"),
1186
+ moreQueueActions: byId("moreQueueActions"),
1187
+ queueMenu: byId("queueMenu"),
1188
+ removeUnselected: byId("removeUnselected"),
1189
+ clearQueue: byId("clearQueue"),
1190
+ captureCurrent: byId(config.quickButtonId || "captureCurrent"),
1191
+ start: byId("start"),
1192
+ cancel: byId("cancel"),
1193
+ progressWrap: byId("progressWrap"),
1194
+ progressText: byId("progressText"),
1195
+ progressCurrent: byId("progressCurrent"),
1196
+ clearBatch: byId("clearBatch"),
1197
+ progressBar: byId("progressBar"),
1198
+ resultList: byId("resultList"),
1199
+ verifyBanner: byId("verifyBanner"),
1200
+ verifyText: byId("verifyText"),
1201
+ skipVerify: byId("skipVerify"),
1202
+ folderNote: byId("folderNote"),
1203
+ quotaText: byId("quotaText"),
1204
+ proLink: byId("proLink"),
1205
+ haveCode: byId("haveCode"),
1206
+ licensePanel: byId("licensePanel"),
1207
+ licenseCode: byId("licenseCode"),
1208
+ activateBtn: byId("activateBtn"),
1209
+ deactivateBtn: byId("deactivateBtn"),
1210
+ status: byId("status")
1211
+ };
1212
+ let pollTimer = null;
1213
+ let queue = [];
1214
+ let queueLocked = false;
1215
+ const prefix = config.messagePrefix;
1216
+ void init();
1217
+ async function init() {
1218
+ el.urls.addEventListener("input", updateUrlCount);
1219
+ el.addUrls.addEventListener("click", addInputUrls);
1220
+ el.scanSite.addEventListener("click", scanCurrentSite);
1221
+ el.selectAll.addEventListener("click", () => setAllSelected(true));
1222
+ el.selectNone.addEventListener("click", () => setAllSelected(false));
1223
+ el.moreQueueActions.addEventListener("click", () => el.queueMenu.classList.toggle("hidden"));
1224
+ el.removeUnselected.addEventListener("click", removeUnselected);
1225
+ el.clearQueue.addEventListener("click", clearQueue);
1226
+ el.captureCurrent.addEventListener("click", quickCapture);
1227
+ el.start.addEventListener("click", startBatch);
1228
+ el.cancel.addEventListener("click", cancelBatch);
1229
+ el.clearBatch.addEventListener("click", clearBatchResults);
1230
+ el.skipVerify.addEventListener("click", () => send({
1231
+ type: `${prefix}_SKIP_VERIFY`
1232
+ }));
1233
+ el.haveCode.addEventListener("click", () => el.licensePanel.classList.toggle("hidden"));
1234
+ el.activateBtn.addEventListener("click", activateLicense);
1235
+ el.deactivateBtn.addEventListener("click", deactivateLicense);
1236
+ await loadQueue();
1237
+ updateUrlCount();
1238
+ renderQueue();
1239
+ await refreshState();
1240
+ }
1241
+ __name(init, "init");
1242
+ async function activateLicense() {
1243
+ const code = el.licenseCode.value.trim();
1244
+ if (!code) {
1245
+ setStatus("Paste your activation code first", true);
1246
+ return;
1247
+ }
1248
+ el.activateBtn.disabled = true;
1249
+ setStatus("Activating\u2026");
1250
+ const response = await send({
1251
+ type: `${prefix}_ACTIVATE`,
1252
+ code
1253
+ });
1254
+ el.activateBtn.disabled = false;
1255
+ if (!response.ok) {
1256
+ setStatus(response.error || "Activation failed", true);
1257
+ return;
1258
+ }
1259
+ el.licenseCode.value = "";
1260
+ el.licensePanel.classList.add("hidden");
1261
+ renderQuota(response.quota);
1262
+ setStatus("Pro activated \u2014 enjoy unlimited pages!");
1263
+ }
1264
+ __name(activateLicense, "activateLicense");
1265
+ async function deactivateLicense() {
1266
+ const response = await send({
1267
+ type: `${prefix}_CLEAR_LICENSE`
1268
+ });
1269
+ if (response.ok) {
1270
+ renderQuota(response.quota);
1271
+ setStatus("License removed from this device");
1272
+ }
1273
+ }
1274
+ __name(deactivateLicense, "deactivateLicense");
1275
+ function parseUrls() {
1276
+ return normalizeInputUrls(el.urls.value.split(/\s+/));
1277
+ }
1278
+ __name(parseUrls, "parseUrls");
1279
+ function updateUrlCount() {
1280
+ el.urlCount.textContent = String(parseUrls().length);
1281
+ }
1282
+ __name(updateUrlCount, "updateUrlCount");
1283
+ function normalizeInputUrls(values) {
1284
+ const out = [];
1285
+ const seen = /* @__PURE__ */ new Set();
1286
+ for (const value of values) {
1287
+ try {
1288
+ const url = new URL(String(value || "").trim());
1289
+ if (!/^https?:$/.test(url.protocol)) {
1290
+ continue;
1291
+ }
1292
+ url.hash = "";
1293
+ const normalized = url.toString();
1294
+ if (!seen.has(normalized)) {
1295
+ seen.add(normalized);
1296
+ out.push(normalized);
1297
+ }
1298
+ } catch {
1299
+ }
1300
+ }
1301
+ return out;
1302
+ }
1303
+ __name(normalizeInputUrls, "normalizeInputUrls");
1304
+ function addInputUrls() {
1305
+ const urls = parseUrls();
1306
+ if (!urls.length) {
1307
+ setStatus("Paste at least one valid http(s) URL", true);
1308
+ return;
1309
+ }
1310
+ const added = addUrlsToQueue(urls);
1311
+ el.urls.value = "";
1312
+ updateUrlCount();
1313
+ setStatus(added ? `Added ${added} URL${added === 1 ? "" : "s"} to the queue` : "Those URLs are already in the queue");
1314
+ }
1315
+ __name(addInputUrls, "addInputUrls");
1316
+ function addUrlsToQueue(urls) {
1317
+ const existing = new Set(queue.map((item) => item.url));
1318
+ let added = 0;
1319
+ for (const url of normalizeInputUrls(urls)) {
1320
+ if (!existing.has(url) && queue.length < 500) {
1321
+ queue.push({
1322
+ url,
1323
+ selected: true
1324
+ });
1325
+ existing.add(url);
1326
+ added += 1;
1327
+ }
1328
+ }
1329
+ if (added) {
1330
+ saveQueue();
1331
+ renderQueue();
1332
+ }
1333
+ return added;
1334
+ }
1335
+ __name(addUrlsToQueue, "addUrlsToQueue");
1336
+ async function scanCurrentSite() {
1337
+ setStatus("Scanning current tab\u2026");
1338
+ try {
1339
+ const [tab] = await chrome.tabs.query({
1340
+ active: true,
1341
+ currentWindow: true
1342
+ });
1343
+ if (!tab?.id) {
1344
+ throw new Error("No active tab");
1345
+ }
1346
+ const [result] = await chrome.scripting.executeScript({
1347
+ target: {
1348
+ tabId: tab.id
1349
+ },
1350
+ func: collectSameOriginLinks
1351
+ });
1352
+ const scan = result?.result || {
1353
+ urls: [],
1354
+ sitemapCount: 0
1355
+ };
1356
+ const found = scan.urls || [];
1357
+ if (!found.length) {
1358
+ setStatus("No same-site links found on this page", true);
1359
+ return;
1360
+ }
1361
+ const added = addUrlsToQueue(found);
1362
+ const sitemapNote = scan.sitemapCount ? `, including ${scan.sitemapCount} from sitemap` : "";
1363
+ setStatus(`Found ${found.length} same-site URLs${sitemapNote}; added ${added} new`);
1364
+ } catch (error) {
1365
+ setStatus(errText(error), true);
1366
+ }
1367
+ }
1368
+ __name(scanCurrentSite, "scanCurrentSite");
1369
+ async function startBatch() {
1370
+ const urls = queue.filter((item) => item.selected).map((item) => item.url);
1371
+ if (!urls.length) {
1372
+ setStatus("Select at least one URL from the queue", true);
1373
+ return;
1374
+ }
1375
+ el.start.disabled = true;
1376
+ setStatus("Starting\u2026");
1377
+ const response = await send({
1378
+ type: `${prefix}_START`,
1379
+ urls,
1380
+ settings: config.collectSettings()
1381
+ });
1382
+ el.start.disabled = false;
1383
+ if (!response.ok) {
1384
+ setStatus(response.error || "Failed to start", true);
1385
+ return;
1386
+ }
1387
+ renderBatch(response.batch);
1388
+ renderQuota(response.quota);
1389
+ schedulePoll();
1390
+ setStatus(response.batch?.freeLimited ? "Started (list truncated to today's free quota)" : "Batch started");
1391
+ }
1392
+ __name(startBatch, "startBatch");
1393
+ async function quickCapture() {
1394
+ const [tab] = await chrome.tabs.query({
1395
+ active: true,
1396
+ currentWindow: true
1397
+ });
1398
+ if (!tab?.id || !/^https?:\/\//i.test(tab.url || "")) {
1399
+ setStatus("Open an http(s) website first", true);
1400
+ return;
1401
+ }
1402
+ el.captureCurrent.disabled = true;
1403
+ el.start.disabled = true;
1404
+ setStatus(config.texts.quickIng);
1405
+ const response = await send({
1406
+ type: `${prefix}_${config.quickType}`,
1407
+ tabId: tab.id,
1408
+ settings: config.collectQuickSettings()
1409
+ });
1410
+ el.captureCurrent.disabled = false;
1411
+ renderQueue();
1412
+ if (!response.ok || !response.result) {
1413
+ setStatus(response.error || config.texts.quickFailed, true);
1414
+ return;
1415
+ }
1416
+ renderQuota(response.quota);
1417
+ setStatus(config.quickResult(response.result));
1418
+ }
1419
+ __name(quickCapture, "quickCapture");
1420
+ async function cancelBatch() {
1421
+ const response = await send({
1422
+ type: `${prefix}_CANCEL`
1423
+ });
1424
+ if (response.ok) {
1425
+ renderBatch(response.batch);
1426
+ }
1427
+ }
1428
+ __name(cancelBatch, "cancelBatch");
1429
+ async function clearBatchResults() {
1430
+ const response = await send({
1431
+ type: `${prefix}_CLEAR_BATCH`
1432
+ });
1433
+ if (!response.ok) {
1434
+ setStatus(response.error || "Could not clear results", true);
1435
+ return;
1436
+ }
1437
+ renderBatch(response.batch);
1438
+ setStatus("Batch results cleared");
1439
+ }
1440
+ __name(clearBatchResults, "clearBatchResults");
1441
+ async function refreshState() {
1442
+ const response = await send({
1443
+ type: `${prefix}_STATE`
1444
+ });
1445
+ if (!response.ok) {
1446
+ return;
1447
+ }
1448
+ renderQuota(response.quota);
1449
+ renderBatch(response.batch);
1450
+ if (response.batch?.running) {
1451
+ schedulePoll();
1452
+ }
1453
+ }
1454
+ __name(refreshState, "refreshState");
1455
+ function schedulePoll() {
1456
+ if (pollTimer) {
1457
+ clearInterval(pollTimer);
1458
+ }
1459
+ pollTimer = setInterval(async () => {
1460
+ const response = await send({
1461
+ type: `${prefix}_STATE`
1462
+ });
1463
+ if (!response.ok) {
1464
+ return;
1465
+ }
1466
+ renderQuota(response.quota);
1467
+ renderBatch(response.batch);
1468
+ if (!response.batch?.running && pollTimer) {
1469
+ clearInterval(pollTimer);
1470
+ }
1471
+ }, 800);
1472
+ }
1473
+ __name(schedulePoll, "schedulePoll");
1474
+ function renderQuota(quota) {
1475
+ if (!quota) {
1476
+ return;
1477
+ }
1478
+ if (quota.pro) {
1479
+ const until = quota.expiresAt ? ` \xB7 ${String(quota.expiresAt).slice(0, 10)}` : "";
1480
+ el.quotaText.textContent = `unlimited${until}`;
1481
+ el.proLink.classList.add("hidden");
1482
+ el.deactivateBtn.classList.remove("hidden");
1483
+ el.activateBtn.classList.add("hidden");
1484
+ } else {
1485
+ el.quotaText.textContent = `${quota.remaining}/${quota.limit} today`;
1486
+ el.proLink.classList.remove("hidden");
1487
+ el.deactivateBtn.classList.add("hidden");
1488
+ el.activateBtn.classList.remove("hidden");
1489
+ }
1490
+ }
1491
+ __name(renderQuota, "renderQuota");
1492
+ function renderBatch(batch) {
1493
+ if (!batch) {
1494
+ return;
1495
+ }
1496
+ el.start.classList.toggle("hidden", batch.running);
1497
+ el.captureCurrent.classList.toggle("hidden", batch.running);
1498
+ el.cancel.classList.toggle("hidden", !batch.running);
1499
+ queueLocked = batch.running;
1500
+ renderQueue();
1501
+ el.progressWrap.classList.toggle("hidden", !batch.total);
1502
+ el.clearBatch.classList.toggle("hidden", batch.running || !batch.total);
1503
+ if (batch.total) {
1504
+ el.progressText.textContent = `${batch.done} / ${batch.total}`;
1505
+ el.progressCurrent.textContent = batch.current || "";
1506
+ el.progressBar.style.width = `${batch.total ? Math.round(batch.done / batch.total * 100) : 0}%`;
1507
+ }
1508
+ const verify = batch.needVerify;
1509
+ el.verifyBanner.classList.toggle("hidden", !verify);
1510
+ if (verify) {
1511
+ el.verifyText.textContent = `${verify.challenge} \xB7 ${verify.url.replace(/^https?:\/\//, "")}`;
1512
+ }
1513
+ el.resultList.replaceChildren();
1514
+ for (const item of batch.items) {
1515
+ const rowEl = document.createElement("div");
1516
+ rowEl.className = "resultItem";
1517
+ const state = document.createElement("span");
1518
+ state.className = `state ${item.status}`;
1519
+ state.textContent = {
1520
+ pending: "\xB7",
1521
+ running: "\u2026",
1522
+ success: "\u2713",
1523
+ failed: "\u2717"
1524
+ }[item.status] || "\xB7";
1525
+ const url = document.createElement("span");
1526
+ url.className = "url";
1527
+ url.textContent = item.url.replace(/^https?:\/\//, "");
1528
+ rowEl.append(state, url);
1529
+ if (item.error) {
1530
+ const err = document.createElement("span");
1531
+ err.className = "err";
1532
+ err.textContent = item.error.slice(0, 60);
1533
+ rowEl.append(err);
1534
+ }
1535
+ if (item.warning) {
1536
+ const warning = document.createElement("span");
1537
+ warning.className = "warning";
1538
+ warning.textContent = item.warning.slice(0, 100);
1539
+ rowEl.append(warning);
1540
+ }
1541
+ el.resultList.append(rowEl);
1542
+ }
1543
+ if (batch.folder && batch.done > 0) {
1544
+ el.folderNote.classList.remove("hidden");
1545
+ el.folderNote.textContent = `Saved to Downloads/${batch.folder}/`;
1546
+ } else {
1547
+ el.folderNote.classList.add("hidden");
1548
+ el.folderNote.textContent = "";
1549
+ }
1550
+ }
1551
+ __name(renderBatch, "renderBatch");
1552
+ async function loadQueue() {
1553
+ const data = await chrome.storage.local.get(config.queueKey);
1554
+ const saved = Array.isArray(data[config.queueKey]) ? data[config.queueKey] : [];
1555
+ queue = saved.filter((item) => item && typeof item.url === "string").slice(0, 500).map((item) => ({
1556
+ url: item.url,
1557
+ selected: item.selected !== false
1558
+ }));
1559
+ }
1560
+ __name(loadQueue, "loadQueue");
1561
+ function saveQueue() {
1562
+ void chrome.storage.local.set({
1563
+ [config.queueKey]: queue
1564
+ });
1565
+ }
1566
+ __name(saveQueue, "saveQueue");
1567
+ function renderQueue() {
1568
+ const selected = queue.filter((item) => item.selected).length;
1569
+ el.queueSummary.textContent = `${selected}/${queue.length} selected`;
1570
+ el.start.textContent = selected ? config.texts.startSome(selected) : config.texts.startNone;
1571
+ el.start.disabled = selected === 0;
1572
+ el.selectAll.disabled = queueLocked || !queue.length;
1573
+ el.selectNone.disabled = queueLocked || !queue.length;
1574
+ el.moreQueueActions.disabled = queueLocked || !queue.length;
1575
+ el.addUrls.disabled = queueLocked;
1576
+ el.scanSite.disabled = queueLocked;
1577
+ el.captureCurrent.disabled = queueLocked;
1578
+ el.queueList.replaceChildren();
1579
+ if (!queue.length) {
1580
+ const empty = document.createElement("p");
1581
+ empty.className = "queueEmpty";
1582
+ empty.textContent = "Add URLs or scan the current site to begin.";
1583
+ el.queueList.append(empty);
1584
+ return;
1585
+ }
1586
+ queue.forEach((item, index) => {
1587
+ const row = document.createElement("div");
1588
+ row.className = `queueItem${item.selected ? "" : " unselected"}`;
1589
+ const checkbox = document.createElement("input");
1590
+ checkbox.type = "checkbox";
1591
+ checkbox.checked = item.selected;
1592
+ checkbox.disabled = queueLocked;
1593
+ checkbox.setAttribute("aria-label", `Include ${item.url}`);
1594
+ checkbox.addEventListener("change", () => {
1595
+ item.selected = checkbox.checked;
1596
+ saveQueue();
1597
+ renderQueue();
1598
+ });
1599
+ const url = document.createElement("button");
1600
+ url.type = "button";
1601
+ url.className = "queueUrl";
1602
+ url.textContent = item.url.replace(/^https?:\/\//, "");
1603
+ url.title = item.url;
1604
+ url.disabled = queueLocked;
1605
+ url.addEventListener("click", () => editQueueItem(index));
1606
+ const remove = document.createElement("button");
1607
+ remove.type = "button";
1608
+ remove.className = "queueRemove";
1609
+ remove.textContent = "\xD7";
1610
+ remove.title = "Remove URL";
1611
+ remove.disabled = queueLocked;
1612
+ remove.addEventListener("click", () => {
1613
+ queue.splice(index, 1);
1614
+ saveQueue();
1615
+ renderQueue();
1616
+ });
1617
+ row.append(checkbox, url, remove);
1618
+ el.queueList.append(row);
1619
+ });
1620
+ }
1621
+ __name(renderQueue, "renderQueue");
1622
+ function editQueueItem(index) {
1623
+ const current = queue[index];
1624
+ const value = prompt("Edit URL", current.url);
1625
+ if (value === null) {
1626
+ return;
1627
+ }
1628
+ const [normalized] = normalizeInputUrls([
1629
+ value
1630
+ ]);
1631
+ if (!normalized) {
1632
+ setStatus("Enter a valid http(s) URL", true);
1633
+ return;
1634
+ }
1635
+ if (queue.some((item, itemIndex) => itemIndex !== index && item.url === normalized)) {
1636
+ setStatus("That URL is already in the queue", true);
1637
+ return;
1638
+ }
1639
+ current.url = normalized;
1640
+ saveQueue();
1641
+ renderQueue();
1642
+ }
1643
+ __name(editQueueItem, "editQueueItem");
1644
+ function setAllSelected(selected) {
1645
+ if (queueLocked) {
1646
+ return;
1647
+ }
1648
+ queue.forEach((item) => {
1649
+ item.selected = selected;
1650
+ });
1651
+ saveQueue();
1652
+ renderQueue();
1653
+ }
1654
+ __name(setAllSelected, "setAllSelected");
1655
+ function removeUnselected() {
1656
+ queue = queue.filter((item) => item.selected);
1657
+ el.queueMenu.classList.add("hidden");
1658
+ saveQueue();
1659
+ renderQueue();
1660
+ }
1661
+ __name(removeUnselected, "removeUnselected");
1662
+ function clearQueue() {
1663
+ queue = [];
1664
+ el.queueMenu.classList.add("hidden");
1665
+ saveQueue();
1666
+ renderQueue();
1667
+ }
1668
+ __name(clearQueue, "clearQueue");
1669
+ function send(message) {
1670
+ return new Promise((resolve) => {
1671
+ chrome.runtime.sendMessage(message, (response) => {
1672
+ const error = chrome.runtime.lastError;
1673
+ resolve(error ? {
1674
+ ok: false,
1675
+ error: error.message
1676
+ } : response || {
1677
+ ok: false,
1678
+ error: "No response"
1679
+ });
1680
+ });
1681
+ });
1682
+ }
1683
+ __name(send, "send");
1684
+ function setStatus(text, isError = false) {
1685
+ el.status.textContent = text;
1686
+ el.status.classList.toggle("error", isError);
1687
+ }
1688
+ __name(setStatus, "setStatus");
1689
+ function errText(error) {
1690
+ return error instanceof Error ? error.message : String(error);
1691
+ }
1692
+ __name(errText, "errText");
1693
+ }
1694
+ __name(initPopup, "initPopup");
1695
+ async function collectSameOriginLinks() {
1696
+ const seen = /* @__PURE__ */ new Set();
1697
+ const out = [];
1698
+ const origin = location.origin;
1699
+ let sitemapCount = 0;
1700
+ const add = /* @__PURE__ */ __name((value) => {
1701
+ try {
1702
+ const url = new URL(value || "", location.href);
1703
+ url.hash = "";
1704
+ if (url.origin === origin && /^https?:$/.test(url.protocol) && !seen.has(url.toString()) && out.length < 500) {
1705
+ seen.add(url.toString());
1706
+ out.push(url.toString());
1707
+ return true;
1708
+ }
1709
+ } catch {
1710
+ }
1711
+ return false;
1712
+ }, "add");
1713
+ add(location.href);
1714
+ for (const a of document.querySelectorAll("a[href]")) {
1715
+ add(a.getAttribute("href"));
1716
+ if (out.length >= 500) {
1717
+ break;
1718
+ }
1719
+ }
1720
+ const sitemapQueue = [
1721
+ new URL("/sitemap.xml", origin).toString()
1722
+ ];
1723
+ const visitedSitemaps = /* @__PURE__ */ new Set();
1724
+ while (sitemapQueue.length && visitedSitemaps.size < 8 && out.length < 500) {
1725
+ const sitemapUrl = sitemapQueue.shift();
1726
+ if (visitedSitemaps.has(sitemapUrl)) {
1727
+ continue;
1728
+ }
1729
+ visitedSitemaps.add(sitemapUrl);
1730
+ try {
1731
+ const response = await fetch(sitemapUrl, {
1732
+ credentials: "same-origin"
1733
+ });
1734
+ if (!response.ok) {
1735
+ continue;
1736
+ }
1737
+ const xml = new DOMParser().parseFromString(await response.text(), "application/xml");
1738
+ for (const loc of xml.querySelectorAll("loc")) {
1739
+ const value = loc.textContent?.trim();
1740
+ if (!value) {
1741
+ continue;
1742
+ }
1743
+ if (/\.xml(?:\.gz)?(?:$|\?)/i.test(value)) {
1744
+ try {
1745
+ const nested = new URL(value, sitemapUrl);
1746
+ if (nested.origin === origin && !visitedSitemaps.has(nested.toString())) {
1747
+ sitemapQueue.push(nested.toString());
1748
+ }
1749
+ } catch {
1750
+ }
1751
+ } else if (add(value)) {
1752
+ sitemapCount += 1;
1753
+ }
1754
+ if (out.length >= 500) {
1755
+ break;
1756
+ }
1757
+ }
1758
+ } catch {
1759
+ }
1760
+ }
1761
+ return {
1762
+ urls: out,
1763
+ sitemapCount
1764
+ };
1765
+ }
1766
+ __name(collectSameOriginLinks, "collectSameOriginLinks");
1767
+ export {
1768
+ buildBatchFileName,
1769
+ buildQuickFileName,
1770
+ createLicensing,
1771
+ fileStamp,
1772
+ folderNameFor,
1773
+ hostOf,
1774
+ initExtension,
1775
+ initPopup,
1776
+ normalizeUrls,
1777
+ sanitize,
1778
+ sessionStamp,
1779
+ todayString,
1780
+ urlBaseName
1781
+ };