db-gn 1.0.8 → 1.1.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/v2.html +769 -51
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "db-gn",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "The BEST unblocked gaming website! 2000+ Games.",
5
5
  "main": "index.svg",
6
6
  "scripts": {
package/v2.html CHANGED
@@ -257,6 +257,219 @@
257
257
  cursor: not-allowed;
258
258
  }
259
259
 
260
+ #autoclick-menu {
261
+ position: fixed;
262
+ z-index: 60;
263
+ display: none;
264
+ flex-direction: column;
265
+ gap: 4px;
266
+ min-width: 240px;
267
+ max-width: 320px;
268
+ max-height: 60vh;
269
+ overflow-y: auto;
270
+ padding: 6px;
271
+ background: rgba(30, 26, 46, 0.97);
272
+ border: 2px solid rgba(176, 0, 230, 0.45);
273
+ border-radius: 10px;
274
+ box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
275
+ backdrop-filter: blur(6px);
276
+ font-family: 'Comic Sans MS', sans-serif;
277
+ }
278
+
279
+ #autoclick-menu.open {
280
+ display: flex;
281
+ }
282
+
283
+ #autoclick-menu .ac-item {
284
+ appearance: none;
285
+ border: 0;
286
+ background: none;
287
+ font-family: inherit;
288
+ font-size: 15px;
289
+ text-align: left;
290
+ color: #fff;
291
+ padding: 9px 10px;
292
+ border-radius: 6px;
293
+ cursor: pointer;
294
+ transition: background 0.15s ease, color 0.15s ease;
295
+ }
296
+
297
+ #autoclick-menu .ac-item:hover:not(:disabled),
298
+ #autoclick-menu .ac-item:focus-visible {
299
+ background: rgba(176, 0, 230, 0.35);
300
+ outline: none;
301
+ }
302
+
303
+ #autoclick-menu .ac-item:disabled {
304
+ opacity: 0.4;
305
+ cursor: not-allowed;
306
+ }
307
+
308
+ #autoclick-menu .ac-new {
309
+ font-weight: 700;
310
+ border-bottom: 1px solid rgba(255, 255, 255, 0.15);
311
+ border-radius: 6px 6px 0 0;
312
+ }
313
+
314
+ #autoclick-menu .ac-list {
315
+ display: flex;
316
+ flex-direction: column;
317
+ gap: 2px;
318
+ }
319
+
320
+ #autoclick-menu .ac-row {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 4px;
324
+ padding: 2px 4px 2px 0;
325
+ }
326
+
327
+ #autoclick-menu .ac-row.off .ac-name,
328
+ #autoclick-menu .ac-row.off .ac-loc {
329
+ opacity: 0.45;
330
+ }
331
+
332
+ #autoclick-menu .ac-name {
333
+ flex: 1;
334
+ appearance: none;
335
+ border: 0;
336
+ background: none;
337
+ font-family: inherit;
338
+ font-size: 15px;
339
+ text-align: left;
340
+ color: #fff;
341
+ padding: 7px 8px;
342
+ border-radius: 6px;
343
+ cursor: text;
344
+ min-width: 0;
345
+ overflow: hidden;
346
+ text-overflow: ellipsis;
347
+ white-space: nowrap;
348
+ }
349
+
350
+ #autoclick-menu .ac-name:hover,
351
+ #autoclick-menu .ac-name:focus-visible {
352
+ background: rgba(176, 0, 230, 0.35);
353
+ outline: none;
354
+ }
355
+
356
+ #autoclick-menu .ac-name-input {
357
+ flex: 1;
358
+ min-width: 0;
359
+ font-family: inherit;
360
+ font-size: 15px;
361
+ color: #fff;
362
+ padding: 6px 7px;
363
+ border-radius: 6px;
364
+ border: 1px solid rgba(176, 0, 230, 0.7);
365
+ background: rgba(0, 0, 0, 0.35);
366
+ outline: none;
367
+ }
368
+
369
+ #autoclick-menu .ac-loc {
370
+ flex: none;
371
+ appearance: none;
372
+ border: 0;
373
+ background: none;
374
+ font-family: inherit;
375
+ font-size: 13px;
376
+ color: rgba(255, 255, 255, 0.6);
377
+ padding: 4px 6px;
378
+ border-radius: 6px;
379
+ cursor: pointer;
380
+ }
381
+
382
+ #autoclick-menu .ac-loc:hover {
383
+ background: rgba(176, 0, 230, 0.35);
384
+ color: #fff;
385
+ }
386
+
387
+ #autoclick-menu .ac-all {
388
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
389
+ margin-bottom: 2px;
390
+ padding-bottom: 6px;
391
+ }
392
+
393
+ #autoclick-menu .ac-all-label {
394
+ flex: 1;
395
+ padding: 4px 8px;
396
+ font-size: 14px;
397
+ font-weight: 700;
398
+ color: rgba(255, 255, 255, 0.85);
399
+ }
400
+
401
+ #autoclick-menu .ac-switch {
402
+ flex: none;
403
+ appearance: none;
404
+ border: 0;
405
+ width: 34px;
406
+ height: 18px;
407
+ padding: 0;
408
+ border-radius: 999px;
409
+ background: rgba(255, 255, 255, 0.25);
410
+ cursor: pointer;
411
+ position: relative;
412
+ transition: background 0.15s ease;
413
+ }
414
+
415
+ #autoclick-menu .ac-switch.on {
416
+ background: rgba(0, 220, 120, 0.85);
417
+ }
418
+
419
+ #autoclick-menu .ac-switch:disabled {
420
+ opacity: 0.35;
421
+ cursor: not-allowed;
422
+ }
423
+
424
+ #autoclick-menu .ac-knob {
425
+ position: absolute;
426
+ top: 3px;
427
+ left: 3px;
428
+ width: 12px;
429
+ height: 12px;
430
+ border-radius: 50%;
431
+ background: #fff;
432
+ transition: transform 0.15s ease;
433
+ }
434
+
435
+ #autoclick-menu .ac-switch.on .ac-knob {
436
+ transform: translateX(16px);
437
+ }
438
+
439
+ #autoclick-menu .ac-remove {
440
+ appearance: none;
441
+ border: 0;
442
+ background: none;
443
+ color: rgba(255, 255, 255, 0.6);
444
+ font-family: inherit;
445
+ font-size: 18px;
446
+ line-height: 1;
447
+ padding: 6px 8px;
448
+ border-radius: 6px;
449
+ cursor: pointer;
450
+ }
451
+
452
+ #autoclick-menu .ac-remove:hover {
453
+ background: rgba(255, 60, 60, 0.3);
454
+ color: #fff;
455
+ }
456
+
457
+ #autoclick-menu .ac-group {
458
+ padding: 8px 10px 2px;
459
+ font-size: 13px;
460
+ font-weight: 700;
461
+ color: rgba(214, 140, 255, 0.9);
462
+ overflow: hidden;
463
+ text-overflow: ellipsis;
464
+ white-space: nowrap;
465
+ }
466
+
467
+ #autoclick-menu .ac-empty {
468
+ padding: 9px 10px;
469
+ font-size: 14px;
470
+ color: rgba(255, 255, 255, 0.55);
471
+ }
472
+
260
473
  .autoclick-overlay {
261
474
  position: absolute;
262
475
  inset: 0;
@@ -292,6 +505,14 @@
292
505
  z-index: 51;
293
506
  }
294
507
 
508
+ .autoclick-marker.paused {
509
+ border-color: rgba(255, 255, 255, 0.55);
510
+ }
511
+
512
+ .autoclick-marker.paused::after {
513
+ background: rgba(255, 255, 255, 0.55);
514
+ }
515
+
295
516
  .autoclick-marker::after {
296
517
  content: "";
297
518
  position: absolute;
@@ -777,6 +998,24 @@
777
998
  background: #006094;
778
999
  }
779
1000
 
1001
+ .runner-check {
1002
+ display: flex;
1003
+ align-items: center;
1004
+ gap: 8px;
1005
+ margin-top: 10px;
1006
+ color: rgba(255, 255, 255, 0.85);
1007
+ font-size: 14px;
1008
+ cursor: pointer;
1009
+ user-select: none;
1010
+ }
1011
+
1012
+ .runner-check input {
1013
+ width: 16px;
1014
+ height: 16px;
1015
+ accent-color: #03a5fc;
1016
+ cursor: pointer;
1017
+ }
1018
+
780
1019
  .runner-frame {
781
1020
  width: 100%;
782
1021
  height: 100%;
@@ -1036,9 +1275,9 @@
1036
1275
  btn.id = "autoclick-btn";
1037
1276
  btn.type = "button";
1038
1277
  btn.innerHTML = ICON_AUTOCLICK;
1039
- btn.title = "Auto Clicker";
1040
- btn.setAttribute("aria-label", "Auto Clicker");
1041
- btn.onclick = toggleAutoclick;
1278
+ btn.title = "Auto Clickers";
1279
+ btn.setAttribute("aria-label", "Auto Clickers");
1280
+ btn.onclick = toggleAutoclickMenu;
1042
1281
  tabBar.appendChild(btn);
1043
1282
  }
1044
1283
 
@@ -1159,7 +1398,64 @@
1159
1398
 
1160
1399
  const AUTOCLICK_INTERVAL_MS = 1;
1161
1400
 
1162
- const autoclickers = new Map(); // tab id -> clicker state
1401
+ // clicker id -> clicker state. A tab can hold any number of them.
1402
+ const autoclickers = new Map();
1403
+ let autoclickSeq = 0;
1404
+
1405
+ /* Placed clickers survive closing a game: tab id -> [{ name, x, y }].
1406
+ Only the spots are kept, never the on/off state, so a reopened game
1407
+ always comes back with every clicker switched off. */
1408
+ const CLICKERS_KEY = "dancing-bunny-clickers-v1";
1409
+ let savedClickers = {};
1410
+ try {
1411
+ const parsed = JSON.parse(localStorage.getItem(CLICKERS_KEY) || "{}");
1412
+ if (parsed && typeof parsed === "object") savedClickers = parsed;
1413
+ } catch (e) { savedClickers = {}; }
1414
+
1415
+ function persistClickers() {
1416
+ try { localStorage.setItem(CLICKERS_KEY, JSON.stringify(savedClickers)); } catch (e) { }
1417
+ }
1418
+
1419
+ function saveClickersForTab(tabId) {
1420
+ if (!tabId) return;
1421
+ const placed = clickersForTab(tabId)
1422
+ .filter(c => c.mode === "running")
1423
+ .map(c => ({ name: c.name, x: c.x, y: c.y }));
1424
+ if (placed.length) savedClickers[tabId] = placed;
1425
+ else delete savedClickers[tabId];
1426
+ persistClickers();
1427
+ }
1428
+
1429
+ /* Rebuilds a game's saved clickers onto a freshly loaded iframe. They
1430
+ come back parked (off) with their markers in place. */
1431
+ function restoreClickersForTab(tabId, panel, iframe) {
1432
+ const saved = savedClickers[tabId];
1433
+ if (!Array.isArray(saved) || saved.length === 0) return;
1434
+
1435
+ saved.forEach((entry, i) => {
1436
+ if (!entry || typeof entry.x !== "number" || typeof entry.y !== "number") return;
1437
+ const clicker = {
1438
+ id: `ac-${++autoclickSeq}`,
1439
+ tabId,
1440
+ name: entry.name || `Clicker ${i + 1}`,
1441
+ enabled: false,
1442
+ panel,
1443
+ iframe,
1444
+ overlay: null,
1445
+ marker: null,
1446
+ timer: null,
1447
+ x: entry.x,
1448
+ y: entry.y,
1449
+ mode: "running"
1450
+ };
1451
+ autoclickers.set(clicker.id, clicker);
1452
+ clicker.fire = makeClickerFire(clicker);
1453
+ placeClickerMarker(clicker);
1454
+ });
1455
+
1456
+ setAutoclickButtonState();
1457
+ refreshAutoclickMenu();
1458
+ }
1163
1459
 
1164
1460
  function panelTabId(panel) {
1165
1461
  if (!panel || !panel.id.startsWith("panel-")) return null;
@@ -1173,41 +1469,46 @@
1173
1469
  return iframe ? { id: panelTabId(panel), panel, iframe } : null;
1174
1470
  }
1175
1471
 
1176
- function currentAutoclicker() {
1472
+ function clickersForTab(tabId) {
1473
+ return [...autoclickers.values()].filter(c => c.tabId === tabId);
1474
+ }
1475
+
1476
+ function currentAutoclickers() {
1177
1477
  const target = activeGamePanel();
1178
- return target ? autoclickers.get(target.id) || null : null;
1478
+ return target ? clickersForTab(target.id) : [];
1179
1479
  }
1180
1480
 
1181
1481
  function runningAutoclickCount() {
1182
1482
  let count = 0;
1183
- autoclickers.forEach(c => { if (c.mode === "running") count++; });
1483
+ autoclickers.forEach(c => { if (c.mode === "running" && c.enabled) count++; });
1184
1484
  return count;
1185
1485
  }
1186
1486
 
1187
1487
  function setAutoclickButtonState() {
1188
1488
  const btn = document.getElementById("autoclick-btn");
1189
1489
  if (!btn) return;
1190
- const current = currentAutoclicker();
1191
- const mode = current ? current.mode : "off";
1192
- const elsewhere = runningAutoclickCount() - (mode === "running" ? 1 : 0);
1490
+ const mine = currentAutoclickers();
1491
+ const arming = mine.some(c => c.mode === "arming");
1492
+ const runningHere = mine.filter(c => c.mode === "running" && c.enabled).length;
1493
+ const elsewhere = runningAutoclickCount() - runningHere;
1193
1494
 
1194
- btn.classList.toggle("arming", mode === "arming");
1195
- btn.classList.toggle("running", mode === "running");
1495
+ btn.classList.toggle("arming", arming);
1496
+ btn.classList.toggle("running", runningHere > 0);
1196
1497
 
1197
1498
  let title =
1198
- mode === "arming" ? "Auto Clicker: pick a spot (Esc to cancel)" :
1199
- mode === "running" ? "Auto Clicker: running on this tab (click to stop)" :
1200
- "Auto Clicker";
1499
+ arming ? "Auto Clicker: pick a spot (Esc to cancel)" :
1500
+ runningHere > 0 ? `Auto Clickers: ${runningHere} running on this tab` :
1501
+ "Auto Clickers";
1201
1502
  if (elsewhere > 0) {
1202
- title += ` \u2027 ${elsewhere} running in background`;
1503
+ title += ` ${elsewhere} running in background`;
1203
1504
  }
1204
1505
  btn.title = title;
1205
1506
  btn.setAttribute("aria-label", title);
1206
- btn.setAttribute("aria-pressed", mode !== "off" ? "true" : "false");
1207
- btn.classList.toggle("background-active", mode === "off" && elsewhere > 0);
1507
+ btn.setAttribute("aria-pressed", (arming || runningHere > 0) ? "true" : "false");
1508
+ btn.classList.toggle("background-active", runningHere === 0 && !arming && elsewhere > 0);
1208
1509
  }
1209
1510
 
1210
- function stopAutoclick(clicker = currentAutoclicker()) {
1511
+ function stopAutoclick(clicker, opts = {}) {
1211
1512
  if (!clicker) return;
1212
1513
  if (clicker.timer) clearInterval(clicker.timer);
1213
1514
  clicker.timer = null;
@@ -1217,18 +1518,21 @@
1217
1518
  clicker.marker = null;
1218
1519
  clicker.mode = "off";
1219
1520
  autoclickers.delete(clicker.id);
1521
+ // Closing a tab tears its clickers down but must not forget them.
1522
+ if (!opts.keepSaved) saveClickersForTab(clicker.tabId);
1220
1523
  setAutoclickButtonState();
1524
+ refreshAutoclickMenu();
1221
1525
  }
1222
1526
 
1223
1527
  function stopAutoclickForPanel(panel) {
1224
1528
  const id = panelTabId(panel);
1225
- const clicker = id ? autoclickers.get(id) : null;
1226
- if (clicker) stopAutoclick(clicker);
1529
+ if (!id) return;
1530
+ clickersForTab(id).forEach(c => stopAutoclick(c, { keepSaved: true }));
1227
1531
  }
1228
1532
 
1229
1533
  // A picker that never got a spot shouldn't linger on a tab you left.
1230
1534
  function cancelArmingAutoclickers() {
1231
- autoclickers.forEach(clicker => {
1535
+ [...autoclickers.values()].forEach(clicker => {
1232
1536
  if (clicker.mode === "arming") stopAutoclick(clicker);
1233
1537
  });
1234
1538
  }
@@ -1246,7 +1550,10 @@
1246
1550
  }
1247
1551
 
1248
1552
  const clicker = {
1249
- id: target.id,
1553
+ id: `ac-${++autoclickSeq}`,
1554
+ tabId: target.id,
1555
+ name: `Clicker ${clickersForTab(target.id).length + 1}`,
1556
+ enabled: true,
1250
1557
  panel: target.panel,
1251
1558
  iframe: target.iframe,
1252
1559
  overlay: null,
@@ -1260,7 +1567,7 @@
1260
1567
 
1261
1568
  const overlay = document.createElement("div");
1262
1569
  overlay.className = "autoclick-overlay";
1263
- overlay.innerHTML = `<div class="autoclick-hint">Click a spot to auto-click \u2027 Esc to cancel</div>`;
1570
+ overlay.innerHTML = `<div class="autoclick-hint">Click a spot to auto-click Esc to cancel</div>`;
1264
1571
  overlay.addEventListener("click", (e) => {
1265
1572
  const rect = clicker.iframe.getBoundingClientRect();
1266
1573
  startAutoclick(clicker, e.clientX - rect.left, e.clientY - rect.top);
@@ -1272,46 +1579,333 @@
1272
1579
  setAutoclickButtonState();
1273
1580
  }
1274
1581
 
1275
- function startAutoclick(clicker, x, y) {
1276
- clicker.overlay?.remove();
1277
- clicker.overlay = null;
1278
- clicker.x = x;
1279
- clicker.y = y;
1280
- clicker.mode = "running";
1281
-
1282
- const marker = document.createElement("div");
1283
- marker.className = "autoclick-marker";
1284
- const rect = clicker.iframe.getBoundingClientRect();
1285
- const panelRect = clicker.panel.getBoundingClientRect();
1286
- marker.style.left = `${rect.left - panelRect.left + x}px`;
1287
- marker.style.top = `${rect.top - panelRect.top + y}px`;
1288
- clicker.marker = marker;
1289
- clicker.panel.appendChild(marker);
1290
-
1291
- const fire = () => {
1582
+ function makeClickerFire(clicker) {
1583
+ return () => {
1292
1584
  const frame = clicker.iframe;
1293
1585
  // The game was closed or reloaded out from under us.
1294
- if (!frame || !frame.isConnected) { stopAutoclick(clicker); return; }
1586
+ if (!frame || !frame.isConnected) { stopAutoclick(clicker, { keepSaved: true }); return; }
1295
1587
  try {
1296
1588
  frame.contentWindow?.postMessage(
1297
1589
  { type: "AUTO_CLICK", x: clicker.x, y: clicker.y }, "*"
1298
1590
  );
1299
1591
  } catch (e) { }
1300
1592
  };
1593
+ }
1594
+
1595
+ function placeClickerMarker(clicker) {
1596
+ clicker.marker?.remove();
1597
+ const marker = document.createElement("div");
1598
+ marker.className = "autoclick-marker";
1599
+ marker.classList.toggle("paused", !clicker.enabled);
1600
+
1601
+ // A hidden panel measures as 0x0; the frame fills the panel anyway,
1602
+ // so fall back to the raw coordinates.
1603
+ const rect = clicker.iframe.getBoundingClientRect();
1604
+ const panelRect = clicker.panel.getBoundingClientRect();
1605
+ const offsetX = rect.width ? rect.left - panelRect.left : 0;
1606
+ const offsetY = rect.height ? rect.top - panelRect.top : 0;
1607
+ marker.style.left = `${offsetX + clicker.x}px`;
1608
+ marker.style.top = `${offsetY + clicker.y}px`;
1301
1609
 
1302
- fire();
1303
- clicker.timer = setInterval(fire, AUTOCLICK_INTERVAL_MS);
1610
+ clicker.marker = marker;
1611
+ clicker.panel.appendChild(marker);
1612
+ }
1613
+
1614
+ function startAutoclick(clicker, x, y) {
1615
+ clicker.overlay?.remove();
1616
+ clicker.overlay = null;
1617
+ clicker.x = x;
1618
+ clicker.y = y;
1619
+ clicker.mode = "running";
1620
+
1621
+ placeClickerMarker(clicker);
1622
+ clicker.fire = makeClickerFire(clicker);
1623
+
1624
+ if (clicker.enabled) {
1625
+ clicker.fire();
1626
+ clicker.timer = setInterval(clicker.fire, AUTOCLICK_INTERVAL_MS);
1627
+ }
1628
+ saveClickersForTab(clicker.tabId);
1629
+ setAutoclickButtonState();
1630
+ refreshAutoclickMenu();
1631
+ }
1632
+
1633
+ /* Switching a clicker off parks it: the spot is kept, only the firing
1634
+ stops, so it can be switched back on without picking again. */
1635
+ function setClickerEnabled(clicker, enabled) {
1636
+ if (!clicker || clicker.enabled === enabled) return;
1637
+ clicker.enabled = enabled;
1638
+
1639
+ if (!enabled) {
1640
+ if (clicker.timer) clearInterval(clicker.timer);
1641
+ clicker.timer = null;
1642
+ } else if (clicker.mode === "running" && !clicker.timer && clicker.fire) {
1643
+ clicker.fire();
1644
+ clicker.timer = setInterval(clicker.fire, AUTOCLICK_INTERVAL_MS);
1645
+ }
1646
+
1647
+ clicker.marker?.classList.toggle("paused", !enabled);
1304
1648
  setAutoclickButtonState();
1305
1649
  }
1306
1650
 
1307
- function toggleAutoclick() {
1308
- const current = currentAutoclicker();
1309
- if (current) stopAutoclick(current);
1310
- else armAutoclick();
1651
+ /* ---------------- Auto clicker menu ----------------
1652
+ The toolbar button opens a list: a "New Auto Clicker" action on top,
1653
+ then the clickers themselves. On a game tab only that game's clickers
1654
+ are listed; on the home tab every clicker is listed under its game. */
1655
+
1656
+ function ensureAutoclickMenu() {
1657
+ let menu = document.getElementById("autoclick-menu");
1658
+ if (menu) return menu;
1659
+
1660
+ menu = document.createElement("div");
1661
+ menu.id = "autoclick-menu";
1662
+ menu.setAttribute("role", "menu");
1663
+ document.body.appendChild(menu);
1664
+
1665
+ document.addEventListener("mousedown", (e) => {
1666
+ if (!isAutoclickMenuOpen()) return;
1667
+ if (menu.contains(e.target)) return;
1668
+ if (e.target.closest("#autoclick-btn")) return;
1669
+ closeAutoclickMenu();
1670
+ });
1671
+ window.addEventListener("resize", closeAutoclickMenu);
1672
+ window.addEventListener("blur", closeAutoclickMenu);
1673
+
1674
+ return menu;
1675
+ }
1676
+
1677
+ function isAutoclickMenuOpen() {
1678
+ return !!document.getElementById("autoclick-menu")?.classList.contains("open");
1679
+ }
1680
+
1681
+ function closeAutoclickMenu() {
1682
+ document.getElementById("autoclick-menu")?.classList.remove("open");
1683
+ }
1684
+
1685
+ function clickerLocationLabel(clicker) {
1686
+ if (clicker.mode === "arming") return "picking a spot…";
1687
+ return `${Math.round(clicker.x)}, ${Math.round(clicker.y)}`;
1688
+ }
1689
+
1690
+ function tabTitleFor(tabId) {
1691
+ return tabs.get(tabId)?.title || "Game";
1692
+ }
1693
+
1694
+ /* A small on/off switch. `state` is the current value; `onToggle` gets
1695
+ the value it was flipped to. */
1696
+ function makeAutoclickSwitch(state, label, onToggle, disabled = false) {
1697
+ const sw = document.createElement("button");
1698
+ sw.type = "button";
1699
+ sw.className = "ac-switch";
1700
+ sw.setAttribute("role", "switch");
1701
+ sw.setAttribute("aria-checked", state ? "true" : "false");
1702
+ sw.setAttribute("aria-label", label);
1703
+ sw.title = label;
1704
+ sw.classList.toggle("on", !!state);
1705
+ sw.disabled = disabled;
1706
+ sw.innerHTML = `<span class="ac-knob"></span>`;
1707
+ sw.onclick = () => { onToggle(!state); };
1708
+ return sw;
1709
+ }
1710
+
1711
+ /* Clicking a clicker's name swaps it for a text field. Enter or blur
1712
+ commits, Escape puts the old name back. */
1713
+ function startClickerRename(clicker, nameBtn) {
1714
+ const input = document.createElement("input");
1715
+ input.type = "text";
1716
+ input.className = "ac-name-input";
1717
+ input.value = clicker.name;
1718
+ input.maxLength = 40;
1719
+
1720
+ let done = false;
1721
+ const finish = (save) => {
1722
+ if (done) return;
1723
+ done = true;
1724
+ if (save) {
1725
+ const next = input.value.replace(/\s+/g, " ").trim();
1726
+ if (next && next !== clicker.name) {
1727
+ clicker.name = next;
1728
+ saveClickersForTab(clicker.tabId);
1729
+ }
1730
+ }
1731
+ nameBtn.textContent = clicker.name;
1732
+ input.replaceWith(nameBtn);
1733
+ };
1734
+
1735
+ input.addEventListener("keydown", (e) => {
1736
+ e.stopPropagation();
1737
+ if (e.key === "Enter") { e.preventDefault(); finish(true); }
1738
+ else if (e.key === "Escape") { e.preventDefault(); finish(false); }
1739
+ });
1740
+ input.addEventListener("blur", () => finish(true));
1741
+
1742
+ nameBtn.replaceWith(input);
1743
+ input.focus();
1744
+ input.select();
1745
+ }
1746
+
1747
+ function makeAutoclickRow(clicker) {
1748
+ const row = document.createElement("div");
1749
+ row.className = "ac-row";
1750
+ row.classList.toggle("off", !clicker.enabled);
1751
+
1752
+ const nameBtn = document.createElement("button");
1753
+ nameBtn.type = "button";
1754
+ nameBtn.className = "ac-name";
1755
+ nameBtn.textContent = clicker.name;
1756
+ nameBtn.title = "Click to rename";
1757
+ nameBtn.onclick = () => startClickerRename(clicker, nameBtn);
1758
+
1759
+ const loc = document.createElement("button");
1760
+ loc.type = "button";
1761
+ loc.className = "ac-loc";
1762
+ loc.textContent = clickerLocationLabel(clicker);
1763
+ loc.title = "Go to this clicker's tab";
1764
+ loc.onclick = () => {
1765
+ closeAutoclickMenu();
1766
+ if (tabs.has(clicker.tabId)) activateTab(clicker.tabId);
1767
+ };
1768
+
1769
+ const sw = makeAutoclickSwitch(clicker.enabled, `Turn ${clicker.name} ${clicker.enabled ? "off" : "on"}`, (on) => {
1770
+ setClickerEnabled(clicker, on);
1771
+ renderAutoclickMenu();
1772
+ });
1773
+
1774
+ const remove = document.createElement("button");
1775
+ remove.type = "button";
1776
+ remove.className = "ac-remove";
1777
+ remove.textContent = "×";
1778
+ remove.title = "Delete this auto clicker";
1779
+ remove.setAttribute("aria-label", "Delete this auto clicker");
1780
+ remove.onclick = () => { stopAutoclick(clicker); };
1781
+
1782
+ row.appendChild(nameBtn);
1783
+ row.appendChild(loc);
1784
+ row.appendChild(sw);
1785
+ row.appendChild(remove);
1786
+ return row;
1787
+ }
1788
+
1789
+ /* The "all of this game's clickers" switch: on only when every clicker
1790
+ in the group is on. */
1791
+ function makeAutoclickAllRow(label, clickers) {
1792
+ const row = document.createElement("div");
1793
+ row.className = "ac-row ac-all";
1794
+
1795
+ const text = document.createElement("span");
1796
+ text.className = "ac-all-label";
1797
+ text.textContent = label;
1798
+
1799
+ const allOn = clickers.length > 0 && clickers.every(c => c.enabled);
1800
+ const sw = makeAutoclickSwitch(allOn, `Turn all ${label.toLowerCase()} ${allOn ? "off" : "on"}`, (on) => {
1801
+ clickers.forEach(c => setClickerEnabled(c, on));
1802
+ renderAutoclickMenu();
1803
+ }, clickers.length === 0);
1804
+
1805
+ row.appendChild(text);
1806
+ row.appendChild(sw);
1807
+ return row;
1808
+ }
1809
+
1810
+ function renderAutoclickMenu() {
1811
+ const menu = ensureAutoclickMenu();
1812
+ menu.innerHTML = "";
1813
+
1814
+ const target = activeGamePanel();
1815
+
1816
+ const newBtn = document.createElement("button");
1817
+ newBtn.type = "button";
1818
+ newBtn.className = "ac-item ac-new";
1819
+ newBtn.textContent = "New Auto Clicker";
1820
+ if (target) {
1821
+ newBtn.onclick = () => { closeAutoclickMenu(); armAutoclick(); };
1822
+ } else {
1823
+ newBtn.disabled = true;
1824
+ newBtn.title = "Open a game tab first";
1825
+ }
1826
+ menu.appendChild(newBtn);
1827
+
1828
+ const list = document.createElement("div");
1829
+ list.className = "ac-list";
1830
+
1831
+ if (target) {
1832
+ // On a game tab: just this game's clickers, with a master switch.
1833
+ const mine = clickersForTab(target.id);
1834
+ list.appendChild(makeAutoclickAllRow("All Clickers", mine));
1835
+ if (mine.length === 0) {
1836
+ list.appendChild(makeAutoclickEmpty("No auto clickers on this game."));
1837
+ } else {
1838
+ mine.forEach(clicker => list.appendChild(makeAutoclickRow(clicker)));
1839
+ }
1840
+ } else {
1841
+ // On the home tab: everything, grouped under its game's name,
1842
+ // each group with its own master switch.
1843
+ const byTab = new Map();
1844
+ autoclickers.forEach(clicker => {
1845
+ if (!byTab.has(clicker.tabId)) byTab.set(clicker.tabId, []);
1846
+ byTab.get(clicker.tabId).push(clicker);
1847
+ });
1848
+
1849
+ if (byTab.size === 0) {
1850
+ list.appendChild(makeAutoclickEmpty("No auto clickers running."));
1851
+ } else {
1852
+ byTab.forEach((clickers, tabId) => {
1853
+ const header = document.createElement("div");
1854
+ header.className = "ac-group";
1855
+ header.textContent = tabTitleFor(tabId);
1856
+ list.appendChild(header);
1857
+ list.appendChild(makeAutoclickAllRow("All", clickers));
1858
+ clickers.forEach(clicker => list.appendChild(makeAutoclickRow(clicker)));
1859
+ });
1860
+ }
1861
+ }
1862
+
1863
+ menu.appendChild(list);
1864
+ }
1865
+
1866
+ /* Live updates skip the redraw while a name is being typed. */
1867
+ function refreshAutoclickMenu() {
1868
+ const menu = document.getElementById("autoclick-menu");
1869
+ if (!menu || !menu.classList.contains("open")) return;
1870
+ if (menu.querySelector(".ac-name-input")) return;
1871
+ renderAutoclickMenu();
1872
+ }
1873
+
1874
+ function makeAutoclickEmpty(text) {
1875
+ const empty = document.createElement("div");
1876
+ empty.className = "ac-empty";
1877
+ empty.textContent = text;
1878
+ return empty;
1879
+ }
1880
+
1881
+ function openAutoclickMenu() {
1882
+ const btn = document.getElementById("autoclick-btn");
1883
+ const menu = ensureAutoclickMenu();
1884
+ renderAutoclickMenu();
1885
+ menu.classList.add("open");
1886
+
1887
+ if (!btn) return;
1888
+ const anchor = btn.getBoundingClientRect();
1889
+ const rect = menu.getBoundingClientRect();
1890
+ const margin = 8;
1891
+ let left = anchor.right - rect.width;
1892
+ let top = anchor.bottom + 6;
1893
+ left = Math.max(margin, Math.min(left, window.innerWidth - rect.width - margin));
1894
+ top = Math.max(margin, Math.min(top, window.innerHeight - rect.height - margin));
1895
+ menu.style.left = `${left}px`;
1896
+ menu.style.top = `${top}px`;
1897
+ }
1898
+
1899
+ function toggleAutoclickMenu() {
1900
+ if (isAutoclickMenuOpen()) closeAutoclickMenu();
1901
+ else openAutoclickMenu();
1311
1902
  }
1312
1903
 
1313
1904
  document.addEventListener("keydown", (e) => {
1314
- if (e.key === "Escape") stopAutoclick();
1905
+ if (e.key !== "Escape") return;
1906
+ if (document.activeElement?.classList.contains("ac-name-input")) return;
1907
+ closeAutoclickMenu();
1908
+ cancelArmingAutoclickers();
1315
1909
  });
1316
1910
 
1317
1911
  (function protectTabIdentity() {
@@ -1421,6 +2015,7 @@
1421
2015
 
1422
2016
  function activateTab(id) {
1423
2017
  setTabStatus(id, null);
2018
+ closeAutoclickMenu();
1424
2019
  cancelArmingAutoclickers();
1425
2020
  setActiveTabButton(id);
1426
2021
  setActivePanel(id);
@@ -2088,6 +2683,7 @@ ${icons}
2088
2683
  iframe.referrerPolicy = "no-referrer";
2089
2684
  applyMuteToIframe(iframe);
2090
2685
  tab.panel.appendChild(iframe);
2686
+ restoreClickersForTab(id, tab.panel, iframe);
2091
2687
  setTabStatus(id, isTabActive(id) ? null : "ready");
2092
2688
 
2093
2689
  } catch (err) {
@@ -2101,14 +2697,119 @@ ${icons}
2101
2697
  }
2102
2698
  }
2103
2699
 
2700
+ /* Pulls the text of the code's own <title> tag, entities decoded. */
2701
+ function extractDocTitle(htmlCode) {
2702
+ const match = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(htmlCode || "");
2703
+ if (!match) return "";
2704
+ const decoder = document.createElement("textarea");
2705
+ decoder.innerHTML = match[1];
2706
+ return decoder.value.replace(/\s+/g, " ").trim();
2707
+ }
2708
+
2709
+ function escapeHtmlText(text) {
2710
+ return String(text)
2711
+ .replace(/&/g, "&amp;")
2712
+ .replace(/</g, "&lt;")
2713
+ .replace(/>/g, "&gt;")
2714
+ .replace(/"/g, "&quot;");
2715
+ }
2716
+
2717
+ /* The home tab's own favicon links, as markup. */
2718
+ function homeIconsHtml() {
2719
+ return [...document.querySelectorAll("link[rel='icon']")].map(l => l.outerHTML).join("\n");
2720
+ }
2721
+
2722
+ /* Icon links declared in the code. Relative hrefs are dropped: the popup
2723
+ lives on about:blank, so they would resolve to nothing. */
2724
+ function extractIconLinksHtml(htmlCode) {
2725
+ const links = [];
2726
+ const tagPattern = /<link\b[^>]*>/gi;
2727
+ let tag;
2728
+ while ((tag = tagPattern.exec(htmlCode || "")) !== null) {
2729
+ const text = tag[0];
2730
+ const rel = /\brel\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(text);
2731
+ const relValue = rel ? (rel[2] ?? rel[3] ?? rel[4] ?? "") : "";
2732
+ if (!/\bicon\b/i.test(relValue)) continue;
2733
+ const href = /\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(text);
2734
+ const hrefValue = href ? (href[2] ?? href[3] ?? href[4] ?? "").trim() : "";
2735
+ if (!/^(data:|https?:|\/\/)/i.test(hrefValue)) continue;
2736
+ links.push(text);
2737
+ }
2738
+ return links.join("\n");
2739
+ }
2740
+
2741
+ /* Runs the typed HTML in a real browser tab. With cloaking on (or when the
2742
+ code declares no title/icon of its own) the tab wears the home page's
2743
+ title and favicon instead. */
2744
+ function openHtmlInNewWindow(htmlCode, keepCloaking) {
2745
+ const win = window.open("about:blank", "_blank");
2746
+ if (!win) {
2747
+ alert("The popup was blocked. Allow popups for this page to open code in a new tab.");
2748
+ return;
2749
+ }
2750
+
2751
+ const codeTitle = keepCloaking ? "" : extractDocTitle(htmlCode);
2752
+ const codeIcons = keepCloaking ? "" : extractIconLinksHtml(htmlCode);
2753
+ const title = codeTitle || "New tab";
2754
+ const icons = codeIcons || homeIconsHtml();
2755
+
2756
+ const prepared = injectBaseHref(
2757
+ injectOffTabSupport(injectAutoclickSupport(injectMuteSupport(htmlCode, isMuted)), bgRunEnabled),
2758
+ document.baseURI
2759
+ );
2760
+
2761
+ win.document.write(`<!DOCTYPE html>
2762
+ <html>
2763
+ <head>
2764
+ <meta charset="utf-8">
2765
+ <title>${escapeHtmlText(title)}</title>
2766
+ ${icons}
2767
+ <style>
2768
+ html, body { margin: 0; height: 100%; background: #fff; }
2769
+ iframe { border: 0; display: block; width: 100%; height: 100%; }
2770
+ </style>
2771
+ </head>
2772
+ <body>
2773
+ <script>
2774
+ // Same trick as the main page: re-append the icon links so the browser
2775
+ // picks the favicon back up after the document is written.
2776
+ function refreshFavicon() {
2777
+ const existingIcons = document.querySelectorAll("link[rel='icon']");
2778
+ existingIcons.forEach(oldIcon => {
2779
+ const newIcon = oldIcon.cloneNode(true);
2780
+ oldIcon.remove();
2781
+ document.head.appendChild(newIcon);
2782
+ });
2783
+ }
2784
+ setTimeout(refreshFavicon, 51);
2785
+ <\/script>
2786
+ </body>
2787
+ </html>`);
2788
+ win.document.close();
2789
+
2790
+ // document.write resets the title in some browsers; pin it afterwards.
2791
+ try { win.document.title = title; } catch (err) { }
2792
+
2793
+ const iframe = win.document.createElement("iframe");
2794
+ iframe.srcdoc = prepared;
2795
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-modals");
2796
+ iframe.referrerPolicy = "no-referrer";
2797
+ win.document.body.appendChild(iframe);
2798
+
2799
+ try { win.refreshFavicon && win.setTimeout(win.refreshFavicon, 51); } catch (err) { }
2800
+ }
2801
+
2104
2802
  function openHtmlPreviewTab(htmlCode) {
2105
2803
  const id = "html-preview";
2106
- const title = "HTML Code";
2804
+ const title = extractDocTitle(htmlCode) || "HTML Code";
2107
2805
 
2108
2806
  const prepared = injectBaseHref(injectAutoclickSupport(injectMuteSupport(htmlCode, isMuted)), document.baseURI);
2109
2807
 
2110
2808
  if (tabs.has(id)) {
2111
2809
  const existing = tabs.get(id);
2810
+ existing.title = title;
2811
+ const label = existing.button.querySelector(".tab-title");
2812
+ if (label) label.textContent = sanitizeTitle(title);
2112
2813
  existing.panel.innerHTML = "";
2113
2814
  const iframe = document.createElement("iframe");
2114
2815
  iframe.className = "game-frame";
@@ -2184,9 +2885,14 @@ ${icons}
2184
2885
  &lt;/body&gt;
2185
2886
  &lt;/html&gt;</textarea>
2186
2887
  <div class="runner-controls">
2187
- <button id="runner-run-btn">Open in New Tab</button>
2888
+ <button id="runner-run-btn">Open in New Subtab</button>
2889
+ <button id="runner-newtab-btn">Open in New Tab</button>
2188
2890
  <button id="runner-clear-btn">Clear</button>
2189
2891
  </div>
2892
+ <label class="runner-check" for="runner-cloak">
2893
+ <input type="checkbox" id="runner-cloak" checked>
2894
+ <span>Keep Cloaking</span>
2895
+ </label>
2190
2896
  </div>
2191
2897
  </div>
2192
2898
  `;
@@ -2201,12 +2907,18 @@ ${icons}
2201
2907
 
2202
2908
  const code = panel.querySelector("#runner-code");
2203
2909
  const runBtn = panel.querySelector("#runner-run-btn");
2910
+ const newTabBtn = panel.querySelector("#runner-newtab-btn");
2911
+ const cloakBox = panel.querySelector("#runner-cloak");
2204
2912
  const clearBtn = panel.querySelector("#runner-clear-btn");
2205
2913
 
2206
2914
  runBtn.onclick = () => {
2207
2915
  openHtmlPreviewTab(code.value);
2208
2916
  };
2209
2917
 
2918
+ newTabBtn.onclick = () => {
2919
+ openHtmlInNewWindow(code.value, cloakBox.checked);
2920
+ };
2921
+
2210
2922
  clearBtn.onclick = () => {
2211
2923
  code.value = "";
2212
2924
  };
@@ -2232,6 +2944,7 @@ ${icons}
2232
2944
  <textarea class="runner-textarea" placeholder="Paste your bookmarklet javascript: here..."></textarea>
2233
2945
  <div class="runner-controls">
2234
2946
  <button class="run-btn">Run Bookmarklet</button>
2947
+ <button class="clear-btn">Clear</button>
2235
2948
  </div>
2236
2949
  </div>
2237
2950
  </div>
@@ -2239,6 +2952,11 @@ ${icons}
2239
2952
 
2240
2953
  const textarea = tab.panel.querySelector(".runner-textarea");
2241
2954
  const runBtn = tab.panel.querySelector(".run-btn");
2955
+ const clearBtn = tab.panel.querySelector(".clear-btn");
2956
+
2957
+ clearBtn.addEventListener("click", () => {
2958
+ textarea.value = "";
2959
+ });
2242
2960
 
2243
2961
  runBtn.addEventListener("click", () => {
2244
2962
  let code = textarea.value.trim();