db-gn 1.0.7 → 1.0.8

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 +485 -258
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "db-gn",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "The BEST unblocked gaming website! 2000+ Games.",
5
5
  "main": "index.svg",
6
6
  "scripts": {
package/v2.html CHANGED
@@ -413,10 +413,122 @@
413
413
 
414
414
  .main-content {
415
415
  min-height: 100%;
416
- padding: 20px 22px 50px;
416
+ /* extra bottom room so the fixed corner links never sit on content */
417
+ padding: 20px 22px 80px;
417
418
  position: relative;
418
419
  }
419
420
 
421
+ /* Pinned to the viewport, so the row holds its corner while the list
422
+ scrolls. Lives inside #home-pane so it hides along with it. */
423
+ #game-context-menu {
424
+ position: fixed;
425
+ z-index: 60;
426
+ display: none;
427
+ flex-direction: column;
428
+ min-width: 210px;
429
+ padding: 6px;
430
+ background: rgba(30, 26, 46, 0.97);
431
+ border: 2px solid rgba(176, 0, 230, 0.45);
432
+ border-radius: 10px;
433
+ box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
434
+ backdrop-filter: blur(6px);
435
+ font-family: 'Comic Sans MS', sans-serif;
436
+ }
437
+
438
+ #game-context-menu.open {
439
+ display: flex;
440
+ }
441
+
442
+ #game-context-menu .ctx-item {
443
+ appearance: none;
444
+ border: 0;
445
+ background: none;
446
+ font-family: inherit;
447
+ font-size: 15px;
448
+ text-align: left;
449
+ color: #fff;
450
+ padding: 9px 10px;
451
+ border-radius: 6px;
452
+ cursor: pointer;
453
+ transition: background 0.15s ease, color 0.15s ease;
454
+ }
455
+
456
+ #game-context-menu .ctx-item:hover,
457
+ #game-context-menu .ctx-item:focus-visible {
458
+ background: rgba(176, 0, 230, 0.35);
459
+ color: #fff;
460
+ outline: none;
461
+ }
462
+
463
+ #corner-links {
464
+ position: fixed;
465
+ right: 20px;
466
+ bottom: 14px;
467
+ z-index: 5;
468
+ display: flex;
469
+ flex-wrap: wrap;
470
+ justify-content: flex-end;
471
+ align-items: baseline;
472
+ gap: 6px 20px;
473
+ }
474
+
475
+ .corner-link {
476
+ display: inline-flex;
477
+ align-items: baseline;
478
+ gap: 6px;
479
+ margin: 0;
480
+ padding: 0;
481
+ border: 0;
482
+ background: none;
483
+ font-family: 'Comic Sans MS', sans-serif;
484
+ font-size: 16px;
485
+ line-height: 1.4;
486
+ cursor: pointer;
487
+ color: rgba(255, 255, 255, 0.6);
488
+ text-decoration: none;
489
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
490
+ transition: color 0.2s ease;
491
+ }
492
+
493
+ /* Underline lives on the text itself: text-decoration on an inline-flex
494
+ container does not reliably reach its flex items. It stays
495
+ transparent until hover/focus so the row reads clean at rest. */
496
+ .corner-link span {
497
+ text-decoration: underline;
498
+ text-decoration-color: transparent;
499
+ text-decoration-thickness: 1px;
500
+ text-underline-offset: 3px;
501
+ transition: color 0.2s ease, text-decoration-color 0.2s ease;
502
+ }
503
+
504
+ #github-link svg {
505
+ width: 17px;
506
+ height: 17px;
507
+ flex-shrink: 0;
508
+ /* nudge the icon down so it sits on the text baseline */
509
+ transform: translateY(2px);
510
+ }
511
+
512
+ .corner-link:hover,
513
+ .corner-link:focus-visible {
514
+ color: #d98cff;
515
+ }
516
+
517
+ .corner-link:hover span,
518
+ .corner-link:focus-visible span {
519
+ text-decoration-color: #d98cff;
520
+ }
521
+
522
+ #github-link:visited {
523
+ color: rgba(255, 255, 255, 0.6);
524
+ }
525
+
526
+ #github-link:visited:hover {
527
+ color: #d98cff;
528
+ }
529
+
530
+
531
+
420
532
 
421
533
  /* ==========================================================================
422
534
  6. Typography & Headings
@@ -609,112 +721,6 @@
609
721
  font-size: 20px;
610
722
  }
611
723
 
612
- .recent-pane {
613
- height: 100%;
614
- overflow-y: auto;
615
- box-sizing: border-box;
616
- padding: 32px 20px 48px;
617
- color: white;
618
- background: linear-gradient(135deg, #2e3244 30%, #561a86 100%);
619
- background-attachment: fixed;
620
- }
621
-
622
- .recent-inner {
623
- max-width: 900px;
624
- margin: 0 auto;
625
- }
626
-
627
- .recent-header {
628
- text-align: center;
629
- margin-bottom: 8px;
630
- font-size: 32px;
631
- font-weight: bold;
632
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
633
- }
634
-
635
- .recent-subheader {
636
- text-align: center;
637
- margin: 0 0 24px;
638
- font-size: 15px;
639
- color: rgba(255, 255, 255, 0.55);
640
- padding-bottom: 18px;
641
- border-bottom: 3px solid rgba(255, 255, 255, 0.15);
642
- }
643
-
644
- .recent-list {
645
- display: grid;
646
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
647
- gap: 12px;
648
- }
649
-
650
- .recent-card {
651
- display: flex;
652
- align-items: center;
653
- gap: 14px;
654
- width: 100%;
655
- padding: 14px 16px;
656
- box-sizing: border-box;
657
- text-align: left;
658
- font-size: 16px;
659
- font-weight: 500;
660
- color: white;
661
- background: rgba(255, 255, 255, 0.08);
662
- border: 2px solid rgba(255, 255, 255, 0.12);
663
- border-radius: 12px;
664
- cursor: pointer;
665
- backdrop-filter: blur(5px);
666
- transition: all 0.25s ease;
667
- }
668
-
669
- .recent-card:hover {
670
- background: rgba(176, 0, 230, 0.25);
671
- border-color: rgba(176, 0, 230, 0.7);
672
- transform: translateY(-2px);
673
- box-shadow: 0 6px 15px rgba(176, 0, 230, 0.35);
674
- }
675
-
676
- .recent-card:active {
677
- transform: translateY(0);
678
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
679
- }
680
-
681
- .recent-rank {
682
- display: inline-flex;
683
- align-items: center;
684
- justify-content: center;
685
- flex-shrink: 0;
686
- width: 32px;
687
- height: 32px;
688
- border-radius: 50%;
689
- background: rgba(0, 0, 0, 0.3);
690
- border: 1px solid rgba(255, 255, 255, 0.15);
691
- font-size: 14px;
692
- color: rgba(255, 255, 255, 0.6);
693
- }
694
-
695
- .recent-card:hover .recent-rank {
696
- color: white;
697
- border-color: rgba(255, 255, 255, 0.5);
698
- }
699
-
700
- .recent-name {
701
- flex: 1;
702
- min-width: 0;
703
- overflow: hidden;
704
- text-overflow: ellipsis;
705
- white-space: nowrap;
706
- }
707
-
708
- .recent-empty {
709
- text-align: center;
710
- padding: 60px 20px;
711
- color: rgba(255, 255, 255, 0.5);
712
- font-size: 17px;
713
- line-height: 1.7;
714
- border: 2px dashed rgba(255, 255, 255, 0.15);
715
- border-radius: 14px;
716
- }
717
-
718
724
  .runner-pane {
719
725
  display: flex;
720
726
  flex-direction: column;
@@ -780,13 +786,9 @@
780
786
  }
781
787
 
782
788
  /* Common layout configuration for external operational utility keys */
783
- #open-runner-btn,
784
789
  #open-proxy-btn,
785
790
  #force-open-proxy-btn,
786
- #open-form-btn,
787
- #doc-btn,
788
- #open-recent-btn,
789
- #open-bookmarklet-runner-btn {
791
+ #doc-btn {
790
792
  font-family: Comic Sans MS, sans-serif;
791
793
  background: none;
792
794
  border: 2px solid rgba(176, 0, 230 0.25);
@@ -801,12 +803,8 @@
801
803
  margin-left: 8px;
802
804
  }
803
805
 
804
- #open-runner-btn:hover,
805
806
  #open-proxy-btn:hover,
806
- #open-form-btn:hover,
807
- #doc-btn:hover,
808
- #open-recent-btn:hover,
809
- #open-bookmarklet-runner-btn:hover {
807
+ #doc-btn:hover {
810
808
  transform: translateY(-2px);
811
809
  box-shadow: 0 6px 15px rgba(176, 0, 230, 0.4);
812
810
  }
@@ -851,7 +849,7 @@
851
849
  }
852
850
 
853
851
  .main-content {
854
- padding: 16px 12px 42px;
852
+ padding: 16px 12px 110px;
855
853
  }
856
854
 
857
855
  .runner-grid {
@@ -875,62 +873,6 @@
875
873
  Made by a Turtle
876
874
  </h2>
877
875
 
878
- <h2 class="subheading">
879
- Documentation:
880
- <button id="doc-btn" onclick="
881
- fetch('https://cdn.jsdelivr.net/gh/ltorl/dancing-bunny@latest/docs/documentation.html')
882
- .then(response => response.text())
883
- .then(text => {
884
- const newWin = window.open('about:blank', '_blank');
885
- if (newWin) {
886
- newWin.document.open();
887
- newWin.document.write(text);
888
- newWin.document.close();
889
- }
890
- });">
891
- Dancing Bunny GN Doc
892
- </button>
893
- </h2>
894
-
895
- <h2 class="subheading">
896
- Run HTML Code:
897
- <button id="open-runner-btn" class="btn-outline">Open Code Runner</button>
898
- </h2>
899
-
900
- <h2 class="subheading">
901
- Run Bookmarklet Code:
902
- <button id="open-bookmarklet-runner-btn" style="line-height: 2; margin-left: 8px;">
903
- Open Bookmarklet Runner
904
- </button>
905
- </h2>
906
-
907
- <!--
908
- <h2 class="subheading">
909
- Other Apps:
910
- <select id="proxy-selector">
911
- <option value="https://truffled.lol/">Truffled</option>
912
- <option value="https://incog.works/">Incognito</option>
913
- <option value="https://gointospace.app/">Interstellar</option>
914
- <option value="https://anura.pro/">Anura</option>
915
- <option value="https://definitelyscience.com/">Definitely Science</option>
916
- </select>
917
-
918
- <button class="btn-outline" id="open-proxy-btn" onclick="openSelectedProxy()">Open</button>
919
- <button class="btn-outline" onclick="openSelectedProxyForceIframe()" id="force-open-proxy-btn">Force
920
- Iframe</button>
921
- </h2>
922
- -->
923
-
924
- <h2 class="subheading">
925
- Recently Played:
926
- <button id="open-recent-btn" class="btn-outline">Open Recent Games</button>
927
- </h2>
928
-
929
- <h2 class="subheading">
930
- Make a sugestion:
931
- <button id="open-form-btn" class="btn-outline">Open Form</button>
932
- </h2>
933
-
934
876
  <br /><br />
935
877
 
936
878
  <div class="search-container">
@@ -940,6 +882,20 @@
940
882
 
941
883
  <div id="sections-container"></div>
942
884
  </div>
885
+
886
+ <div id="corner-links">
887
+ <button type="button" id="open-runner-btn" class="corner-link"><span>HTML Runner</span></button>
888
+ <button type="button" id="open-bookmarklet-runner-btn" class="corner-link"><span>Bookmarklet Runner</span></button>
889
+ <button type="button" id="open-form-btn" class="corner-link"><span>Make a Suggestion</span></button>
890
+ <a id="github-link" class="corner-link" href="https://github.com/ltorl/dancing-bunny" target="_blank" rel="noopener noreferrer"
891
+ title="View the source on GitHub" aria-label="View the source on GitHub">
892
+ <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
893
+ <path fill="currentColor"
894
+ d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
895
+ </svg>
896
+ <span>Github</span>
897
+ </a>
898
+ </div>
943
899
  </div>
944
900
  </div>
945
901
 
@@ -971,7 +927,7 @@
971
927
  const MUTE_KEY = "dancing-bunny-muted-v1";
972
928
  const RECENT_KEY = "dancing-bunny-recent-v1";
973
929
  const BGRUN_KEY = "dancing-bunny-bgrun-v1";
974
- const RECENT_MAX = 20;
930
+ const RECENT_MAX = 15;
975
931
 
976
932
  // When true, game tabs keep rendering (and therefore keep running) while
977
933
  // another tab is on screen. Turning it off pauses them to save CPU.
@@ -1016,7 +972,7 @@
1016
972
  function recordRecent(file) {
1017
973
  recent = [file, ...recent.filter(f => f !== file)].slice(0, RECENT_MAX);
1018
974
  saveRecent();
1019
- renderRecentPane();
975
+ renderRecentSection();
1020
976
  }
1021
977
 
1022
978
  function saveMute() {
@@ -1105,11 +1061,21 @@
1105
1061
 
1106
1062
  function applyBackgroundRunState() {
1107
1063
  document.body.classList.toggle("no-bg-run", !bgRunEnabled);
1064
+
1065
+ document.querySelectorAll("iframe").forEach(frame => {
1066
+ try {
1067
+ frame.contentWindow?.postMessage({ type: "OFF_TAB_RUN", enabled: bgRunEnabled }, "*");
1068
+ if (typeof frame.contentWindow?.__setOffTabRun === "function") {
1069
+ frame.contentWindow.__setOffTabRun(bgRunEnabled);
1070
+ }
1071
+ } catch (e) { }
1072
+ });
1073
+
1108
1074
  const btn = document.getElementById("bgrun-btn");
1109
1075
  if (btn) {
1110
1076
  btn.classList.toggle("off", !bgRunEnabled);
1111
1077
  btn.title = bgRunEnabled
1112
- ? "Background games: ON (they keep running on other tabs)"
1078
+ ? "Background games: ON (they keep running on other subtabs and while this browser tab is hidden)"
1113
1079
  : "Background games: OFF (paused when not visible)";
1114
1080
  btn.setAttribute("aria-label", btn.title);
1115
1081
  btn.setAttribute("aria-pressed", bgRunEnabled ? "true" : "false");
@@ -1191,7 +1157,7 @@
1191
1157
  background games keep getting clicked; only an unplaced picker is
1192
1158
  cancelled when you leave its tab. */
1193
1159
 
1194
- const AUTOCLICK_INTERVAL_MS = 100;
1160
+ const AUTOCLICK_INTERVAL_MS = 1;
1195
1161
 
1196
1162
  const autoclickers = new Map(); // tab id -> clicker state
1197
1163
 
@@ -1668,6 +1634,138 @@
1668
1634
  return `${muteScript}${html}`;
1669
1635
  }
1670
1636
 
1637
+ /* Browsers freeze requestAnimationFrame and clamp timers once the whole
1638
+ browser tab is hidden, which stalls idle/clicker games even though our
1639
+ in-page subtabs keep them mounted. This shim keeps them ticking off-tab
1640
+ by driving their callbacks from a Web Worker's clock instead. It only
1641
+ takes over while the page is hidden, so visible games run on the real
1642
+ rAF, and it dies with the page like anything else. */
1643
+ function injectOffTabSupport(html, enabled = true) {
1644
+ const script = `
1645
+ <script>
1646
+ (function() {
1647
+ if (window.__OFF_TAB_TICKER) return;
1648
+ window.__OFF_TAB_TICKER = true;
1649
+
1650
+ var TICK_MS = 16;
1651
+ var enabled = ${enabled ? "true" : "false"};
1652
+
1653
+ window.addEventListener("message", function(e) {
1654
+ var data = e.data;
1655
+ if (data && data.type === "OFF_TAB_RUN") enabled = !!data.enabled;
1656
+ });
1657
+ window.__setOffTabRun = function(value) { enabled = !!value; };
1658
+
1659
+ function hidden() { return document.visibilityState === "hidden"; }
1660
+ function active() { return enabled && hidden(); }
1661
+
1662
+ /* Worker timers keep running at full rate in a backgrounded page far longer
1663
+ than page timers do; setInterval is the fallback when Workers are blocked. */
1664
+ var tickHandlers = [];
1665
+ function onTick() {
1666
+ for (var i = 0; i < tickHandlers.length; i++) {
1667
+ try { tickHandlers[i](); } catch (e) {}
1668
+ }
1669
+ }
1670
+
1671
+ try {
1672
+ var workerSrc = "var id=null;onmessage=function(e){if(e.data==='start'){if(id===null)id=setInterval(function(){postMessage(0);}," + TICK_MS + ");}else{clearInterval(id);id=null;}};";
1673
+ var worker = new Worker(URL.createObjectURL(new Blob([workerSrc], { type: "text/javascript" })));
1674
+ worker.onmessage = onTick;
1675
+ worker.postMessage("start");
1676
+ } catch (e) {
1677
+ setInterval(onTick, TICK_MS);
1678
+ }
1679
+
1680
+ /* ---------- requestAnimationFrame ---------- */
1681
+ var nativeRaf = window.requestAnimationFrame && window.requestAnimationFrame.bind(window);
1682
+ var nativeCaf = window.cancelAnimationFrame && window.cancelAnimationFrame.bind(window);
1683
+
1684
+ if (nativeRaf) {
1685
+ var SHIM_BASE = 1000000000; // keep our ids clear of the native ones
1686
+ var nextRafId = SHIM_BASE;
1687
+ var rafQueue = new Map();
1688
+
1689
+ window.requestAnimationFrame = function(cb) {
1690
+ if (!active()) return nativeRaf(cb);
1691
+ var id = nextRafId++;
1692
+ rafQueue.set(id, cb);
1693
+ return id;
1694
+ };
1695
+
1696
+ window.cancelAnimationFrame = function(id) {
1697
+ if (id >= SHIM_BASE) { rafQueue.delete(id); return; }
1698
+ if (nativeCaf) nativeCaf(id);
1699
+ };
1700
+
1701
+ tickHandlers.push(function() {
1702
+ if (rafQueue.size === 0) return;
1703
+ var due = rafQueue;
1704
+ rafQueue = new Map();
1705
+ var stamp = performance.now();
1706
+ due.forEach(function(cb) {
1707
+ try { cb(stamp); } catch (e) {}
1708
+ });
1709
+ });
1710
+ }
1711
+
1712
+ /* ---------- setTimeout / setInterval ---------- */
1713
+ var nativeSetTimeout = window.setTimeout.bind(window);
1714
+ var nativeClearTimeout = window.clearTimeout.bind(window);
1715
+ var nativeSetInterval = window.setInterval.bind(window);
1716
+ var nativeClearInterval = window.clearInterval.bind(window);
1717
+
1718
+ var TIMER_BASE = 2000000000;
1719
+ var nextTimerId = TIMER_BASE;
1720
+ var timers = new Map();
1721
+
1722
+ function schedule(fn, delay, args, repeating) {
1723
+ var wait = Math.max(0, Number(delay) || 0);
1724
+ var id = nextTimerId++;
1725
+ timers.set(id, { fn: fn, args: args, due: performance.now() + wait, every: repeating ? wait : null });
1726
+ return id;
1727
+ }
1728
+
1729
+ window.setTimeout = function(fn, delay) {
1730
+ var args = Array.prototype.slice.call(arguments, 2);
1731
+ if (!active() || typeof fn !== "function") return nativeSetTimeout.apply(window, arguments);
1732
+ return schedule(fn, delay, args, false);
1733
+ };
1734
+
1735
+ window.setInterval = function(fn, delay) {
1736
+ var args = Array.prototype.slice.call(arguments, 2);
1737
+ if (!active() || typeof fn !== "function") return nativeSetInterval.apply(window, arguments);
1738
+ return schedule(fn, delay, args, true);
1739
+ };
1740
+
1741
+ window.clearTimeout = function(id) {
1742
+ if (id >= TIMER_BASE) { timers.delete(id); return; }
1743
+ nativeClearTimeout(id);
1744
+ };
1745
+
1746
+ window.clearInterval = function(id) {
1747
+ if (id >= TIMER_BASE) { timers.delete(id); return; }
1748
+ nativeClearInterval(id);
1749
+ };
1750
+
1751
+ tickHandlers.push(function() {
1752
+ if (timers.size === 0) return;
1753
+ var now = performance.now();
1754
+ timers.forEach(function(timer, id) {
1755
+ if (now < timer.due) return;
1756
+ if (timer.every === null) timers.delete(id);
1757
+ else timer.due = now + timer.every;
1758
+ try { timer.fn.apply(window, timer.args); } catch (e) {}
1759
+ });
1760
+ });
1761
+ })();
1762
+ <\/script>`;
1763
+
1764
+ if (/<\/body>/i.test(html)) return html.replace(/<\/body>/i, `${script}</body>`);
1765
+ if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, `${script}</head>`);
1766
+ return `${html}${script}`;
1767
+ }
1768
+
1671
1769
  function injectAutoclickSupport(html) {
1672
1770
  const clickScript = `
1673
1771
  <script>
@@ -1741,6 +1839,164 @@
1741
1839
  e.preventDefault();
1742
1840
  openGameTab(file, { background: true });
1743
1841
  });
1842
+ el.addEventListener("contextmenu", (e) => {
1843
+ e.preventDefault();
1844
+ openGameContextMenu(e, file);
1845
+ });
1846
+ }
1847
+
1848
+ /* ---------------- right-click menu ---------------- */
1849
+
1850
+ let ctxMenu = null;
1851
+
1852
+ function hideGameContextMenu() {
1853
+ if (ctxMenu) ctxMenu.classList.remove("open");
1854
+ }
1855
+
1856
+ function ensureGameContextMenu() {
1857
+ if (ctxMenu) return ctxMenu;
1858
+
1859
+ ctxMenu = document.createElement("div");
1860
+ ctxMenu.id = "game-context-menu";
1861
+ ctxMenu.setAttribute("role", "menu");
1862
+ document.body.appendChild(ctxMenu);
1863
+
1864
+ // Any interaction outside the menu dismisses it.
1865
+ document.addEventListener("mousedown", (e) => {
1866
+ if (ctxMenu.classList.contains("open") && !ctxMenu.contains(e.target)) hideGameContextMenu();
1867
+ });
1868
+ document.addEventListener("keydown", (e) => {
1869
+ if (e.key === "Escape") hideGameContextMenu();
1870
+ });
1871
+ window.addEventListener("resize", hideGameContextMenu);
1872
+ window.addEventListener("blur", hideGameContextMenu);
1873
+ document.addEventListener("scroll", hideGameContextMenu, true);
1874
+
1875
+ return ctxMenu;
1876
+ }
1877
+
1878
+ function openGameContextMenu(e, file) {
1879
+ const menu = ensureGameContextMenu();
1880
+ menu.innerHTML = "";
1881
+
1882
+ const items = [
1883
+ ["Open", () => openGameTab(file)],
1884
+ ["Open Game in New Subtab", () => openGameTab(file, { background: true })],
1885
+ ["Open Game in New Tab", () => openGameInNewWindow(file)],
1886
+ ];
1887
+
1888
+ items.forEach(([label, action]) => {
1889
+ const item = document.createElement("button");
1890
+ item.type = "button";
1891
+ item.className = "ctx-item";
1892
+ item.setAttribute("role", "menuitem");
1893
+ item.textContent = label;
1894
+ item.onclick = () => { hideGameContextMenu(); action(); };
1895
+ menu.appendChild(item);
1896
+ });
1897
+
1898
+ // Show first so the box has a measurable size, then clamp on-screen.
1899
+ menu.classList.add("open");
1900
+ menu.style.left = "0px";
1901
+ menu.style.top = "0px";
1902
+ const rect = menu.getBoundingClientRect();
1903
+ const x = Math.min(e.clientX, window.innerWidth - rect.width - 8);
1904
+ const y = Math.min(e.clientY, window.innerHeight - rect.height - 8);
1905
+ menu.style.left = `${Math.max(8, x)}px`;
1906
+ menu.style.top = `${Math.max(8, y)}px`;
1907
+
1908
+ const first = menu.querySelector(".ctx-item");
1909
+ if (first) first.focus();
1910
+ }
1911
+
1912
+ /* Plain fetch across the proxy list — the in-tab loader logs its progress
1913
+ into the tab, but a popup has nowhere to put that. */
1914
+ async function fetchGameSource(file) {
1915
+ const encoded = encodeURIComponent(normalizeFileName(file));
1916
+ for (const base of GAME_BASES) {
1917
+ try {
1918
+ const response = await fetch(`${base}${encoded}?t=${Date.now()}`);
1919
+ if (!response.ok) continue;
1920
+ const text = await response.text();
1921
+ const clean = text.trim().toLowerCase();
1922
+ if (clean === "404: not found" || clean === "not found" || clean.includes("404 page not found")) continue;
1923
+ return { text, base };
1924
+ } catch (e) { /* try the next proxy */ }
1925
+ }
1926
+ return null;
1927
+ }
1928
+
1929
+ /* Opens the game in a real browser tab on about:blank, wearing the home
1930
+ tab's title and favicon rather than the game's. */
1931
+ async function openGameInNewWindow(file) {
1932
+ const win = window.open("about:blank", "_blank");
1933
+ if (!win) {
1934
+ alert("The popup was blocked. Allow popups for this page to open games in a new tab.");
1935
+ return;
1936
+ }
1937
+
1938
+ const title = displayNameFromFile(file);
1939
+ const icons = [...document.querySelectorAll("link[rel='icon']")].map(l => l.outerHTML).join("\n");
1940
+
1941
+ win.document.write(`<!DOCTYPE html>
1942
+ <html>
1943
+ <head>
1944
+ <meta charset="utf-8">
1945
+ <title>New tab</title>
1946
+ ${icons}
1947
+ <style>
1948
+ html, body { margin: 0; height: 100%; background: linear-gradient(135deg, #2e3244 30%, #561a86 100%); }
1949
+ iframe { border: 0; display: block; width: 100%; height: 100%; }
1950
+ #loading { display: grid; place-items: center; height: 100%; color: #fff; font-family: 'Comic Sans MS', sans-serif; font-size: 20px; }
1951
+ </style>
1952
+ </head>
1953
+ <body><div id="loading">Loading ${sanitizeTitle(title)}...</div>
1954
+ <script>
1955
+ // Same trick as the main page: re-append the icon links so the browser
1956
+ // picks the favicon back up after the document is written.
1957
+ function refreshFavicon() {
1958
+ const existingIcons = document.querySelectorAll("link[rel='icon']");
1959
+ existingIcons.forEach(oldIcon => {
1960
+ const newIcon = oldIcon.cloneNode(true);
1961
+ oldIcon.remove();
1962
+ document.head.appendChild(newIcon);
1963
+ });
1964
+ }
1965
+ setTimeout(refreshFavicon, 51);
1966
+ <\/script>
1967
+ </body>
1968
+ </html>`);
1969
+ win.document.close();
1970
+
1971
+ // document.write resets the title in some browsers; pin it afterwards.
1972
+ try { win.document.title = "New tab"; } catch (err) { }
1973
+
1974
+ recordRecent(file);
1975
+
1976
+ const source = await fetchGameSource(file);
1977
+ if (win.closed) return;
1978
+
1979
+ const loading = win.document.getElementById("loading");
1980
+
1981
+ if (!source) {
1982
+ if (loading) {
1983
+ loading.textContent = `Failed to load ${sanitizeTitle(title)}.`;
1984
+ loading.style.color = "#ff5555";
1985
+ }
1986
+ return;
1987
+ }
1988
+
1989
+ let html = injectOffTabSupport(injectAutoclickSupport(injectMuteSupport(source.text, isMuted)), bgRunEnabled);
1990
+ html = injectBaseHref(html, source.base);
1991
+
1992
+ if (loading) loading.remove();
1993
+ const iframe = win.document.createElement("iframe");
1994
+ iframe.srcdoc = html;
1995
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-modals");
1996
+ iframe.referrerPolicy = "no-referrer";
1997
+ win.document.body.appendChild(iframe);
1998
+
1999
+ try { win.refreshFavicon && win.setTimeout(win.refreshFavicon, 51); } catch (err) { }
1744
2000
  }
1745
2001
 
1746
2002
  async function openGameTab(file, opts = {}) {
@@ -1821,7 +2077,7 @@
1821
2077
 
1822
2078
  if (!text) throw new Error("All proxy servers failed, were skipped, or the file does not exist.");
1823
2079
 
1824
- let html = injectAutoclickSupport(injectMuteSupport(text, isMuted));
2080
+ let html = injectOffTabSupport(injectAutoclickSupport(injectMuteSupport(text, isMuted)), bgRunEnabled);
1825
2081
  html = injectBaseHref(html, successBase);
1826
2082
 
1827
2083
  tab.panel.innerHTML = "";
@@ -1894,82 +2150,6 @@
1894
2150
  activateTab(id);
1895
2151
  }
1896
2152
 
1897
- function renderRecentPane() {
1898
- const list = document.getElementById("recent-list");
1899
- if (!list) return;
1900
-
1901
- list.innerHTML = "";
1902
-
1903
- const count = document.getElementById("recent-count");
1904
-
1905
- if (recent.length === 0) {
1906
- list.style.display = "block";
1907
- if (count) count.textContent = " ";
1908
- const empty = document.createElement("div");
1909
- empty.className = "recent-empty";
1910
- empty.textContent = "No games played yet";
1911
- list.appendChild(empty);
1912
- return;
1913
- }
1914
-
1915
- list.style.display = "";
1916
- if (count) count.textContent = ` `;
1917
-
1918
- recent.forEach((file, i) => {
1919
- const card = document.createElement("button");
1920
- card.className = "recent-card";
1921
- card.dataset.file = file;
1922
- bindGameLauncher(card, file);
1923
-
1924
- const rank = document.createElement("span");
1925
- rank.className = "recent-rank";
1926
- rank.textContent = i + 1;
1927
-
1928
- const name = document.createElement("span");
1929
- name.className = "recent-name";
1930
- name.textContent = displayNameFromFile(file);
1931
- name.title = name.textContent;
1932
-
1933
- card.appendChild(rank);
1934
- card.appendChild(name);
1935
- list.appendChild(card);
1936
- });
1937
- }
1938
-
1939
- function openRecentGamesTab() {
1940
- const id = "recent-games";
1941
- const title = "Recent Games";
1942
-
1943
- if (tabs.has(id)) { activateTab(id); renderRecentPane(); return; }
1944
-
1945
- const button = createTabButton(id, title, true);
1946
- tabBar.insertBefore(button, tabBarAnchor());
1947
-
1948
- const panel = document.createElement("div");
1949
- panel.className = "panel game-pane static-pane";
1950
- panel.id = `panel-${id}`;
1951
-
1952
- const wrapper = document.createElement("div");
1953
- wrapper.className = "recent-pane";
1954
- wrapper.innerHTML = `
1955
- <div class="recent-inner">
1956
- <h2 class="recent-header">Recently Played</h2>
1957
- <p class="recent-subheader" id="recent-count"></p>
1958
- <div class="recent-list" id="recent-list"></div>
1959
- </div>
1960
- `;
1961
-
1962
- panel.appendChild(wrapper);
1963
- contentArea.appendChild(panel);
1964
-
1965
- const tab = { id, title, button, panel };
1966
- tabs.set(id, tab);
1967
- tabOrder.push(id);
1968
- activateTab(id);
1969
-
1970
- renderRecentPane();
1971
- }
1972
-
1973
2153
  function openRunnerTab() {
1974
2154
  const id = "runner";
1975
2155
  const title = "HTML Runner";
@@ -2134,13 +2314,44 @@
2134
2314
  const container = document.getElementById("sections-container");
2135
2315
  const firstRealSection = container.firstElementChild;
2136
2316
  container.insertBefore(section, firstRealSection);
2317
+
2318
+ filterGames();
2319
+ }
2320
+
2321
+ function renderRecentSection() {
2322
+ const container = document.getElementById("sections-container");
2323
+ if (!container) return;
2324
+
2325
+ const existing = document.getElementById("section-recent");
2326
+ if (existing) existing.remove();
2327
+
2328
+ const recentSidebarBtn = document.getElementById("sidebar-recent");
2329
+ if (recentSidebarBtn) recentSidebarBtn.style.opacity = recent.length > 0 ? "1" : "0.3";
2330
+
2331
+ if (recent.length === 0) return;
2332
+
2333
+ const section = document.createElement("div");
2334
+ section.className = "letter-section"; section.id = "section-recent";
2335
+
2336
+ const header = document.createElement("div");
2337
+ header.className = "letter-header"; header.textContent = "Recently Played"; section.appendChild(header);
2338
+
2339
+ const buttonsContainer = document.createElement("div");
2340
+ buttonsContainer.className = "buttons-container";
2341
+ recent.forEach(file => buttonsContainer.appendChild(makeGameRow(file)));
2342
+ section.appendChild(buttonsContainer);
2343
+
2344
+ const fav = document.getElementById("section-fav");
2345
+ container.insertBefore(section, fav ? fav.nextElementSibling : container.firstElementChild);
2346
+
2347
+ filterGames();
2137
2348
  }
2138
2349
 
2139
2350
  function generateSidebar(allChars, filesByChar) {
2140
2351
  const sidebar = document.getElementById("sidebar"); sidebar.innerHTML = "";
2141
2352
 
2142
2353
  const searchBtn = document.createElement("button");
2143
- searchBtn.className = "sidebar-btn"; searchBtn.textContent = "Search";
2354
+ searchBtn.className = "sidebar-btn"; searchBtn.textContent = "search";
2144
2355
  searchBtn.onclick = () => document.getElementById("gameSearch").scrollIntoView({ behavior: "smooth", block: "end" });
2145
2356
  sidebar.appendChild(searchBtn);
2146
2357
 
@@ -2153,6 +2364,15 @@
2153
2364
  };
2154
2365
  sidebar.appendChild(favBtn);
2155
2366
 
2367
+ const recentBtn = document.createElement("button");
2368
+ recentBtn.className = "sidebar-btn"; recentBtn.id = "sidebar-recent"; recentBtn.textContent = "recent";
2369
+ recentBtn.style.opacity = recent.length > 0 ? "1" : "0.3";
2370
+ recentBtn.onclick = () => {
2371
+ const section = document.getElementById("section-recent");
2372
+ if (section) section.scrollIntoView({ behavior: "smooth", block: "start" });
2373
+ };
2374
+ sidebar.appendChild(recentBtn);
2375
+
2156
2376
  allChars.forEach((char) => {
2157
2377
  if (char === "Search") return;
2158
2378
  const btn = document.createElement("button");
@@ -2200,11 +2420,12 @@
2200
2420
  section.appendChild(buttonsContainer); container.appendChild(section);
2201
2421
  });
2202
2422
 
2203
- generateSidebar(sidebarChars, filesByChar); renderFavoritesSection();
2423
+ generateSidebar(sidebarChars, filesByChar); renderFavoritesSection(); renderRecentSection();
2204
2424
  }
2205
2425
 
2206
2426
  function filterGames() {
2207
2427
  const input = document.getElementById("gameSearch");
2428
+ if (!input) return;
2208
2429
  const filter = input.value.toLowerCase();
2209
2430
  const sections = document.querySelectorAll(".letter-section");
2210
2431
 
@@ -2226,7 +2447,6 @@
2226
2447
  }
2227
2448
 
2228
2449
  document.getElementById("open-runner-btn").addEventListener("click", openRunnerTab);
2229
- document.getElementById("open-recent-btn").addEventListener("click", openRecentGamesTab);
2230
2450
 
2231
2451
  async function fetchAndInjectProxy(targetUrl, tab, title, id) {
2232
2452
  const logArea = tab.panel.querySelector(`#logs-${id}`);
@@ -2361,6 +2581,13 @@
2361
2581
 
2362
2582
  document.getElementById("open-form-btn").addEventListener("click", openFormTab);
2363
2583
 
2584
+ // Ask the browser to show its native "Leave site?" prompt on close/reload.
2585
+ window.addEventListener("beforeunload", (e) => {
2586
+ e.preventDefault();
2587
+ e.returnValue = "";
2588
+ return "";
2589
+ });
2590
+
2364
2591
  function initWhenReady() {
2365
2592
  if (typeof files === "undefined") {
2366
2593
  setTimeout(initWhenReady, 50); return;