db-gn 1.0.6 → 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.
- package/README.md +5 -5
- package/package.json +1 -1
- package/v2.html +485 -258
package/README.md
CHANGED
|
@@ -15,12 +15,10 @@ The project includes:
|
|
|
15
15
|
* Multi-tab game interface
|
|
16
16
|
* Search system
|
|
17
17
|
* Favorites system
|
|
18
|
+
* Autoclicker
|
|
18
19
|
* Bookmarklet launcher
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Embedded launcher methods
|
|
22
|
-
* Lightweight static deployment
|
|
23
|
-
* Multiple mirror deployments
|
|
20
|
+
* NPM svg launcher
|
|
21
|
+
* New tab cloaking
|
|
24
22
|
|
|
25
23
|
The project currently contains access to over **2608 games**.
|
|
26
24
|
|
|
@@ -34,6 +32,8 @@ The project currently contains access to over **2608 games**.
|
|
|
34
32
|
|
|
35
33
|
## Launch Methods
|
|
36
34
|
|
|
35
|
+
---
|
|
36
|
+
|
|
37
37
|
### NPM svg
|
|
38
38
|
|
|
39
39
|
```text
|
package/package.json
CHANGED
package/v2.html
CHANGED
|
@@ -413,10 +413,122 @@
|
|
|
413
413
|
|
|
414
414
|
.main-content {
|
|
415
415
|
min-height: 100%;
|
|
416
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 = "
|
|
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;
|