db-gn 1.0.7 → 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.
- package/package.json +1 -1
- package/v2.html +1314 -369
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;
|
|
@@ -413,10 +634,122 @@
|
|
|
413
634
|
|
|
414
635
|
.main-content {
|
|
415
636
|
min-height: 100%;
|
|
416
|
-
|
|
637
|
+
/* extra bottom room so the fixed corner links never sit on content */
|
|
638
|
+
padding: 20px 22px 80px;
|
|
417
639
|
position: relative;
|
|
418
640
|
}
|
|
419
641
|
|
|
642
|
+
/* Pinned to the viewport, so the row holds its corner while the list
|
|
643
|
+
scrolls. Lives inside #home-pane so it hides along with it. */
|
|
644
|
+
#game-context-menu {
|
|
645
|
+
position: fixed;
|
|
646
|
+
z-index: 60;
|
|
647
|
+
display: none;
|
|
648
|
+
flex-direction: column;
|
|
649
|
+
min-width: 210px;
|
|
650
|
+
padding: 6px;
|
|
651
|
+
background: rgba(30, 26, 46, 0.97);
|
|
652
|
+
border: 2px solid rgba(176, 0, 230, 0.45);
|
|
653
|
+
border-radius: 10px;
|
|
654
|
+
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
|
|
655
|
+
backdrop-filter: blur(6px);
|
|
656
|
+
font-family: 'Comic Sans MS', sans-serif;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
#game-context-menu.open {
|
|
660
|
+
display: flex;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
#game-context-menu .ctx-item {
|
|
664
|
+
appearance: none;
|
|
665
|
+
border: 0;
|
|
666
|
+
background: none;
|
|
667
|
+
font-family: inherit;
|
|
668
|
+
font-size: 15px;
|
|
669
|
+
text-align: left;
|
|
670
|
+
color: #fff;
|
|
671
|
+
padding: 9px 10px;
|
|
672
|
+
border-radius: 6px;
|
|
673
|
+
cursor: pointer;
|
|
674
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
#game-context-menu .ctx-item:hover,
|
|
678
|
+
#game-context-menu .ctx-item:focus-visible {
|
|
679
|
+
background: rgba(176, 0, 230, 0.35);
|
|
680
|
+
color: #fff;
|
|
681
|
+
outline: none;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
#corner-links {
|
|
685
|
+
position: fixed;
|
|
686
|
+
right: 20px;
|
|
687
|
+
bottom: 14px;
|
|
688
|
+
z-index: 5;
|
|
689
|
+
display: flex;
|
|
690
|
+
flex-wrap: wrap;
|
|
691
|
+
justify-content: flex-end;
|
|
692
|
+
align-items: baseline;
|
|
693
|
+
gap: 6px 20px;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.corner-link {
|
|
697
|
+
display: inline-flex;
|
|
698
|
+
align-items: baseline;
|
|
699
|
+
gap: 6px;
|
|
700
|
+
margin: 0;
|
|
701
|
+
padding: 0;
|
|
702
|
+
border: 0;
|
|
703
|
+
background: none;
|
|
704
|
+
font-family: 'Comic Sans MS', sans-serif;
|
|
705
|
+
font-size: 16px;
|
|
706
|
+
line-height: 1.4;
|
|
707
|
+
cursor: pointer;
|
|
708
|
+
color: rgba(255, 255, 255, 0.6);
|
|
709
|
+
text-decoration: none;
|
|
710
|
+
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
|
711
|
+
transition: color 0.2s ease;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/* Underline lives on the text itself: text-decoration on an inline-flex
|
|
715
|
+
container does not reliably reach its flex items. It stays
|
|
716
|
+
transparent until hover/focus so the row reads clean at rest. */
|
|
717
|
+
.corner-link span {
|
|
718
|
+
text-decoration: underline;
|
|
719
|
+
text-decoration-color: transparent;
|
|
720
|
+
text-decoration-thickness: 1px;
|
|
721
|
+
text-underline-offset: 3px;
|
|
722
|
+
transition: color 0.2s ease, text-decoration-color 0.2s ease;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
#github-link svg {
|
|
726
|
+
width: 17px;
|
|
727
|
+
height: 17px;
|
|
728
|
+
flex-shrink: 0;
|
|
729
|
+
/* nudge the icon down so it sits on the text baseline */
|
|
730
|
+
transform: translateY(2px);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.corner-link:hover,
|
|
734
|
+
.corner-link:focus-visible {
|
|
735
|
+
color: #d98cff;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.corner-link:hover span,
|
|
739
|
+
.corner-link:focus-visible span {
|
|
740
|
+
text-decoration-color: #d98cff;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
#github-link:visited {
|
|
744
|
+
color: rgba(255, 255, 255, 0.6);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
#github-link:visited:hover {
|
|
748
|
+
color: #d98cff;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
|
|
420
753
|
|
|
421
754
|
/* ==========================================================================
|
|
422
755
|
6. Typography & Headings
|
|
@@ -609,112 +942,6 @@
|
|
|
609
942
|
font-size: 20px;
|
|
610
943
|
}
|
|
611
944
|
|
|
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
945
|
.runner-pane {
|
|
719
946
|
display: flex;
|
|
720
947
|
flex-direction: column;
|
|
@@ -771,6 +998,24 @@
|
|
|
771
998
|
background: #006094;
|
|
772
999
|
}
|
|
773
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
|
+
|
|
774
1019
|
.runner-frame {
|
|
775
1020
|
width: 100%;
|
|
776
1021
|
height: 100%;
|
|
@@ -780,13 +1025,9 @@
|
|
|
780
1025
|
}
|
|
781
1026
|
|
|
782
1027
|
/* Common layout configuration for external operational utility keys */
|
|
783
|
-
#open-runner-btn,
|
|
784
1028
|
#open-proxy-btn,
|
|
785
1029
|
#force-open-proxy-btn,
|
|
786
|
-
#
|
|
787
|
-
#doc-btn,
|
|
788
|
-
#open-recent-btn,
|
|
789
|
-
#open-bookmarklet-runner-btn {
|
|
1030
|
+
#doc-btn {
|
|
790
1031
|
font-family: Comic Sans MS, sans-serif;
|
|
791
1032
|
background: none;
|
|
792
1033
|
border: 2px solid rgba(176, 0, 230 0.25);
|
|
@@ -801,12 +1042,8 @@
|
|
|
801
1042
|
margin-left: 8px;
|
|
802
1043
|
}
|
|
803
1044
|
|
|
804
|
-
#open-runner-btn:hover,
|
|
805
1045
|
#open-proxy-btn:hover,
|
|
806
|
-
#
|
|
807
|
-
#doc-btn:hover,
|
|
808
|
-
#open-recent-btn:hover,
|
|
809
|
-
#open-bookmarklet-runner-btn:hover {
|
|
1046
|
+
#doc-btn:hover {
|
|
810
1047
|
transform: translateY(-2px);
|
|
811
1048
|
box-shadow: 0 6px 15px rgba(176, 0, 230, 0.4);
|
|
812
1049
|
}
|
|
@@ -851,7 +1088,7 @@
|
|
|
851
1088
|
}
|
|
852
1089
|
|
|
853
1090
|
.main-content {
|
|
854
|
-
padding: 16px 12px
|
|
1091
|
+
padding: 16px 12px 110px;
|
|
855
1092
|
}
|
|
856
1093
|
|
|
857
1094
|
.runner-grid {
|
|
@@ -875,62 +1112,6 @@
|
|
|
875
1112
|
Made by a Turtle
|
|
876
1113
|
</h2>
|
|
877
1114
|
|
|
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
1115
|
<br /><br />
|
|
935
1116
|
|
|
936
1117
|
<div class="search-container">
|
|
@@ -940,6 +1121,20 @@
|
|
|
940
1121
|
|
|
941
1122
|
<div id="sections-container"></div>
|
|
942
1123
|
</div>
|
|
1124
|
+
|
|
1125
|
+
<div id="corner-links">
|
|
1126
|
+
<button type="button" id="open-runner-btn" class="corner-link"><span>HTML Runner</span></button>
|
|
1127
|
+
<button type="button" id="open-bookmarklet-runner-btn" class="corner-link"><span>Bookmarklet Runner</span></button>
|
|
1128
|
+
<button type="button" id="open-form-btn" class="corner-link"><span>Make a Suggestion</span></button>
|
|
1129
|
+
<a id="github-link" class="corner-link" href="https://github.com/ltorl/dancing-bunny" target="_blank" rel="noopener noreferrer"
|
|
1130
|
+
title="View the source on GitHub" aria-label="View the source on GitHub">
|
|
1131
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
|
1132
|
+
<path fill="currentColor"
|
|
1133
|
+
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" />
|
|
1134
|
+
</svg>
|
|
1135
|
+
<span>Github</span>
|
|
1136
|
+
</a>
|
|
1137
|
+
</div>
|
|
943
1138
|
</div>
|
|
944
1139
|
</div>
|
|
945
1140
|
|
|
@@ -971,7 +1166,7 @@
|
|
|
971
1166
|
const MUTE_KEY = "dancing-bunny-muted-v1";
|
|
972
1167
|
const RECENT_KEY = "dancing-bunny-recent-v1";
|
|
973
1168
|
const BGRUN_KEY = "dancing-bunny-bgrun-v1";
|
|
974
|
-
const RECENT_MAX =
|
|
1169
|
+
const RECENT_MAX = 15;
|
|
975
1170
|
|
|
976
1171
|
// When true, game tabs keep rendering (and therefore keep running) while
|
|
977
1172
|
// another tab is on screen. Turning it off pauses them to save CPU.
|
|
@@ -1016,7 +1211,7 @@
|
|
|
1016
1211
|
function recordRecent(file) {
|
|
1017
1212
|
recent = [file, ...recent.filter(f => f !== file)].slice(0, RECENT_MAX);
|
|
1018
1213
|
saveRecent();
|
|
1019
|
-
|
|
1214
|
+
renderRecentSection();
|
|
1020
1215
|
}
|
|
1021
1216
|
|
|
1022
1217
|
function saveMute() {
|
|
@@ -1080,9 +1275,9 @@
|
|
|
1080
1275
|
btn.id = "autoclick-btn";
|
|
1081
1276
|
btn.type = "button";
|
|
1082
1277
|
btn.innerHTML = ICON_AUTOCLICK;
|
|
1083
|
-
btn.title = "Auto
|
|
1084
|
-
btn.setAttribute("aria-label", "Auto
|
|
1085
|
-
btn.onclick =
|
|
1278
|
+
btn.title = "Auto Clickers";
|
|
1279
|
+
btn.setAttribute("aria-label", "Auto Clickers");
|
|
1280
|
+
btn.onclick = toggleAutoclickMenu;
|
|
1086
1281
|
tabBar.appendChild(btn);
|
|
1087
1282
|
}
|
|
1088
1283
|
|
|
@@ -1105,11 +1300,21 @@
|
|
|
1105
1300
|
|
|
1106
1301
|
function applyBackgroundRunState() {
|
|
1107
1302
|
document.body.classList.toggle("no-bg-run", !bgRunEnabled);
|
|
1303
|
+
|
|
1304
|
+
document.querySelectorAll("iframe").forEach(frame => {
|
|
1305
|
+
try {
|
|
1306
|
+
frame.contentWindow?.postMessage({ type: "OFF_TAB_RUN", enabled: bgRunEnabled }, "*");
|
|
1307
|
+
if (typeof frame.contentWindow?.__setOffTabRun === "function") {
|
|
1308
|
+
frame.contentWindow.__setOffTabRun(bgRunEnabled);
|
|
1309
|
+
}
|
|
1310
|
+
} catch (e) { }
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1108
1313
|
const btn = document.getElementById("bgrun-btn");
|
|
1109
1314
|
if (btn) {
|
|
1110
1315
|
btn.classList.toggle("off", !bgRunEnabled);
|
|
1111
1316
|
btn.title = bgRunEnabled
|
|
1112
|
-
? "Background games: ON (they keep running on other
|
|
1317
|
+
? "Background games: ON (they keep running on other subtabs and while this browser tab is hidden)"
|
|
1113
1318
|
: "Background games: OFF (paused when not visible)";
|
|
1114
1319
|
btn.setAttribute("aria-label", btn.title);
|
|
1115
1320
|
btn.setAttribute("aria-pressed", bgRunEnabled ? "true" : "false");
|
|
@@ -1191,9 +1396,66 @@
|
|
|
1191
1396
|
background games keep getting clicked; only an unplaced picker is
|
|
1192
1397
|
cancelled when you leave its tab. */
|
|
1193
1398
|
|
|
1194
|
-
const AUTOCLICK_INTERVAL_MS =
|
|
1399
|
+
const AUTOCLICK_INTERVAL_MS = 1;
|
|
1195
1400
|
|
|
1196
|
-
|
|
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
|
+
}
|
|
1197
1459
|
|
|
1198
1460
|
function panelTabId(panel) {
|
|
1199
1461
|
if (!panel || !panel.id.startsWith("panel-")) return null;
|
|
@@ -1207,145 +1469,443 @@
|
|
|
1207
1469
|
return iframe ? { id: panelTabId(panel), panel, iframe } : null;
|
|
1208
1470
|
}
|
|
1209
1471
|
|
|
1210
|
-
function
|
|
1472
|
+
function clickersForTab(tabId) {
|
|
1473
|
+
return [...autoclickers.values()].filter(c => c.tabId === tabId);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function currentAutoclickers() {
|
|
1211
1477
|
const target = activeGamePanel();
|
|
1212
|
-
return target ?
|
|
1478
|
+
return target ? clickersForTab(target.id) : [];
|
|
1213
1479
|
}
|
|
1214
1480
|
|
|
1215
1481
|
function runningAutoclickCount() {
|
|
1216
1482
|
let count = 0;
|
|
1217
|
-
autoclickers.forEach(c => { if (c.mode === "running") count++; });
|
|
1483
|
+
autoclickers.forEach(c => { if (c.mode === "running" && c.enabled) count++; });
|
|
1218
1484
|
return count;
|
|
1219
1485
|
}
|
|
1220
1486
|
|
|
1221
1487
|
function setAutoclickButtonState() {
|
|
1222
1488
|
const btn = document.getElementById("autoclick-btn");
|
|
1223
1489
|
if (!btn) return;
|
|
1224
|
-
const
|
|
1225
|
-
const
|
|
1226
|
-
const
|
|
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;
|
|
1494
|
+
|
|
1495
|
+
btn.classList.toggle("arming", arming);
|
|
1496
|
+
btn.classList.toggle("running", runningHere > 0);
|
|
1497
|
+
|
|
1498
|
+
let title =
|
|
1499
|
+
arming ? "Auto Clicker: pick a spot (Esc to cancel)" :
|
|
1500
|
+
runningHere > 0 ? `Auto Clickers: ${runningHere} running on this tab` :
|
|
1501
|
+
"Auto Clickers";
|
|
1502
|
+
if (elsewhere > 0) {
|
|
1503
|
+
title += ` ‧ ${elsewhere} running in background`;
|
|
1504
|
+
}
|
|
1505
|
+
btn.title = title;
|
|
1506
|
+
btn.setAttribute("aria-label", title);
|
|
1507
|
+
btn.setAttribute("aria-pressed", (arming || runningHere > 0) ? "true" : "false");
|
|
1508
|
+
btn.classList.toggle("background-active", runningHere === 0 && !arming && elsewhere > 0);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function stopAutoclick(clicker, opts = {}) {
|
|
1512
|
+
if (!clicker) return;
|
|
1513
|
+
if (clicker.timer) clearInterval(clicker.timer);
|
|
1514
|
+
clicker.timer = null;
|
|
1515
|
+
clicker.overlay?.remove();
|
|
1516
|
+
clicker.marker?.remove();
|
|
1517
|
+
clicker.overlay = null;
|
|
1518
|
+
clicker.marker = null;
|
|
1519
|
+
clicker.mode = "off";
|
|
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);
|
|
1523
|
+
setAutoclickButtonState();
|
|
1524
|
+
refreshAutoclickMenu();
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function stopAutoclickForPanel(panel) {
|
|
1528
|
+
const id = panelTabId(panel);
|
|
1529
|
+
if (!id) return;
|
|
1530
|
+
clickersForTab(id).forEach(c => stopAutoclick(c, { keepSaved: true }));
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// A picker that never got a spot shouldn't linger on a tab you left.
|
|
1534
|
+
function cancelArmingAutoclickers() {
|
|
1535
|
+
[...autoclickers.values()].forEach(clicker => {
|
|
1536
|
+
if (clicker.mode === "arming") stopAutoclick(clicker);
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function armAutoclick() {
|
|
1541
|
+
const target = activeGamePanel();
|
|
1542
|
+
if (!target) {
|
|
1543
|
+
const btn = document.getElementById("autoclick-btn");
|
|
1544
|
+
if (btn) {
|
|
1545
|
+
const prev = btn.title;
|
|
1546
|
+
btn.title = "Open a game tab first";
|
|
1547
|
+
setTimeout(() => { btn.title = prev; }, 1500);
|
|
1548
|
+
}
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
const clicker = {
|
|
1553
|
+
id: `ac-${++autoclickSeq}`,
|
|
1554
|
+
tabId: target.id,
|
|
1555
|
+
name: `Clicker ${clickersForTab(target.id).length + 1}`,
|
|
1556
|
+
enabled: true,
|
|
1557
|
+
panel: target.panel,
|
|
1558
|
+
iframe: target.iframe,
|
|
1559
|
+
overlay: null,
|
|
1560
|
+
marker: null,
|
|
1561
|
+
timer: null,
|
|
1562
|
+
x: 0,
|
|
1563
|
+
y: 0,
|
|
1564
|
+
mode: "arming"
|
|
1565
|
+
};
|
|
1566
|
+
autoclickers.set(clicker.id, clicker);
|
|
1567
|
+
|
|
1568
|
+
const overlay = document.createElement("div");
|
|
1569
|
+
overlay.className = "autoclick-overlay";
|
|
1570
|
+
overlay.innerHTML = `<div class="autoclick-hint">Click a spot to auto-click ‧ Esc to cancel</div>`;
|
|
1571
|
+
overlay.addEventListener("click", (e) => {
|
|
1572
|
+
const rect = clicker.iframe.getBoundingClientRect();
|
|
1573
|
+
startAutoclick(clicker, e.clientX - rect.left, e.clientY - rect.top);
|
|
1574
|
+
});
|
|
1575
|
+
overlay.addEventListener("contextmenu", (e) => { e.preventDefault(); stopAutoclick(clicker); });
|
|
1576
|
+
|
|
1577
|
+
clicker.overlay = overlay;
|
|
1578
|
+
target.panel.appendChild(overlay);
|
|
1579
|
+
setAutoclickButtonState();
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function makeClickerFire(clicker) {
|
|
1583
|
+
return () => {
|
|
1584
|
+
const frame = clicker.iframe;
|
|
1585
|
+
// The game was closed or reloaded out from under us.
|
|
1586
|
+
if (!frame || !frame.isConnected) { stopAutoclick(clicker, { keepSaved: true }); return; }
|
|
1587
|
+
try {
|
|
1588
|
+
frame.contentWindow?.postMessage(
|
|
1589
|
+
{ type: "AUTO_CLICK", x: clicker.x, y: clicker.y }, "*"
|
|
1590
|
+
);
|
|
1591
|
+
} catch (e) { }
|
|
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`;
|
|
1609
|
+
|
|
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);
|
|
1648
|
+
setAutoclickButtonState();
|
|
1649
|
+
}
|
|
1650
|
+
|
|
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
|
+
};
|
|
1227
1768
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1769
|
+
const sw = makeAutoclickSwitch(clicker.enabled, `Turn ${clicker.name} ${clicker.enabled ? "off" : "on"}`, (on) => {
|
|
1770
|
+
setClickerEnabled(clicker, on);
|
|
1771
|
+
renderAutoclickMenu();
|
|
1772
|
+
});
|
|
1230
1773
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
}
|
|
1238
|
-
btn.title = title;
|
|
1239
|
-
btn.setAttribute("aria-label", title);
|
|
1240
|
-
btn.setAttribute("aria-pressed", mode !== "off" ? "true" : "false");
|
|
1241
|
-
btn.classList.toggle("background-active", mode === "off" && elsewhere > 0);
|
|
1242
|
-
}
|
|
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); };
|
|
1243
1781
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
clicker.marker?.remove();
|
|
1250
|
-
clicker.overlay = null;
|
|
1251
|
-
clicker.marker = null;
|
|
1252
|
-
clicker.mode = "off";
|
|
1253
|
-
autoclickers.delete(clicker.id);
|
|
1254
|
-
setAutoclickButtonState();
|
|
1782
|
+
row.appendChild(nameBtn);
|
|
1783
|
+
row.appendChild(loc);
|
|
1784
|
+
row.appendChild(sw);
|
|
1785
|
+
row.appendChild(remove);
|
|
1786
|
+
return row;
|
|
1255
1787
|
}
|
|
1256
1788
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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";
|
|
1262
1794
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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;
|
|
1268
1808
|
}
|
|
1269
1809
|
|
|
1270
|
-
function
|
|
1810
|
+
function renderAutoclickMenu() {
|
|
1811
|
+
const menu = ensureAutoclickMenu();
|
|
1812
|
+
menu.innerHTML = "";
|
|
1813
|
+
|
|
1271
1814
|
const target = activeGamePanel();
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
}
|
|
1279
|
-
|
|
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";
|
|
1280
1825
|
}
|
|
1826
|
+
menu.appendChild(newBtn);
|
|
1281
1827
|
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
panel: target.panel,
|
|
1285
|
-
iframe: target.iframe,
|
|
1286
|
-
overlay: null,
|
|
1287
|
-
marker: null,
|
|
1288
|
-
timer: null,
|
|
1289
|
-
x: 0,
|
|
1290
|
-
y: 0,
|
|
1291
|
-
mode: "arming"
|
|
1292
|
-
};
|
|
1293
|
-
autoclickers.set(clicker.id, clicker);
|
|
1828
|
+
const list = document.createElement("div");
|
|
1829
|
+
list.className = "ac-list";
|
|
1294
1830
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
+
});
|
|
1303
1848
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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);
|
|
1307
1864
|
}
|
|
1308
1865
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
+
}
|
|
1315
1873
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
clicker.marker = marker;
|
|
1323
|
-
clicker.panel.appendChild(marker);
|
|
1874
|
+
function makeAutoclickEmpty(text) {
|
|
1875
|
+
const empty = document.createElement("div");
|
|
1876
|
+
empty.className = "ac-empty";
|
|
1877
|
+
empty.textContent = text;
|
|
1878
|
+
return empty;
|
|
1879
|
+
}
|
|
1324
1880
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
frame.contentWindow?.postMessage(
|
|
1331
|
-
{ type: "AUTO_CLICK", x: clicker.x, y: clicker.y }, "*"
|
|
1332
|
-
);
|
|
1333
|
-
} catch (e) { }
|
|
1334
|
-
};
|
|
1881
|
+
function openAutoclickMenu() {
|
|
1882
|
+
const btn = document.getElementById("autoclick-btn");
|
|
1883
|
+
const menu = ensureAutoclickMenu();
|
|
1884
|
+
renderAutoclickMenu();
|
|
1885
|
+
menu.classList.add("open");
|
|
1335
1886
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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`;
|
|
1339
1897
|
}
|
|
1340
1898
|
|
|
1341
|
-
function
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
else armAutoclick();
|
|
1899
|
+
function toggleAutoclickMenu() {
|
|
1900
|
+
if (isAutoclickMenuOpen()) closeAutoclickMenu();
|
|
1901
|
+
else openAutoclickMenu();
|
|
1345
1902
|
}
|
|
1346
1903
|
|
|
1347
1904
|
document.addEventListener("keydown", (e) => {
|
|
1348
|
-
if (e.key
|
|
1905
|
+
if (e.key !== "Escape") return;
|
|
1906
|
+
if (document.activeElement?.classList.contains("ac-name-input")) return;
|
|
1907
|
+
closeAutoclickMenu();
|
|
1908
|
+
cancelArmingAutoclickers();
|
|
1349
1909
|
});
|
|
1350
1910
|
|
|
1351
1911
|
(function protectTabIdentity() {
|
|
@@ -1455,6 +2015,7 @@
|
|
|
1455
2015
|
|
|
1456
2016
|
function activateTab(id) {
|
|
1457
2017
|
setTabStatus(id, null);
|
|
2018
|
+
closeAutoclickMenu();
|
|
1458
2019
|
cancelArmingAutoclickers();
|
|
1459
2020
|
setActiveTabButton(id);
|
|
1460
2021
|
setActivePanel(id);
|
|
@@ -1668,6 +2229,138 @@
|
|
|
1668
2229
|
return `${muteScript}${html}`;
|
|
1669
2230
|
}
|
|
1670
2231
|
|
|
2232
|
+
/* Browsers freeze requestAnimationFrame and clamp timers once the whole
|
|
2233
|
+
browser tab is hidden, which stalls idle/clicker games even though our
|
|
2234
|
+
in-page subtabs keep them mounted. This shim keeps them ticking off-tab
|
|
2235
|
+
by driving their callbacks from a Web Worker's clock instead. It only
|
|
2236
|
+
takes over while the page is hidden, so visible games run on the real
|
|
2237
|
+
rAF, and it dies with the page like anything else. */
|
|
2238
|
+
function injectOffTabSupport(html, enabled = true) {
|
|
2239
|
+
const script = `
|
|
2240
|
+
<script>
|
|
2241
|
+
(function() {
|
|
2242
|
+
if (window.__OFF_TAB_TICKER) return;
|
|
2243
|
+
window.__OFF_TAB_TICKER = true;
|
|
2244
|
+
|
|
2245
|
+
var TICK_MS = 16;
|
|
2246
|
+
var enabled = ${enabled ? "true" : "false"};
|
|
2247
|
+
|
|
2248
|
+
window.addEventListener("message", function(e) {
|
|
2249
|
+
var data = e.data;
|
|
2250
|
+
if (data && data.type === "OFF_TAB_RUN") enabled = !!data.enabled;
|
|
2251
|
+
});
|
|
2252
|
+
window.__setOffTabRun = function(value) { enabled = !!value; };
|
|
2253
|
+
|
|
2254
|
+
function hidden() { return document.visibilityState === "hidden"; }
|
|
2255
|
+
function active() { return enabled && hidden(); }
|
|
2256
|
+
|
|
2257
|
+
/* Worker timers keep running at full rate in a backgrounded page far longer
|
|
2258
|
+
than page timers do; setInterval is the fallback when Workers are blocked. */
|
|
2259
|
+
var tickHandlers = [];
|
|
2260
|
+
function onTick() {
|
|
2261
|
+
for (var i = 0; i < tickHandlers.length; i++) {
|
|
2262
|
+
try { tickHandlers[i](); } catch (e) {}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
try {
|
|
2267
|
+
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;}};";
|
|
2268
|
+
var worker = new Worker(URL.createObjectURL(new Blob([workerSrc], { type: "text/javascript" })));
|
|
2269
|
+
worker.onmessage = onTick;
|
|
2270
|
+
worker.postMessage("start");
|
|
2271
|
+
} catch (e) {
|
|
2272
|
+
setInterval(onTick, TICK_MS);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
/* ---------- requestAnimationFrame ---------- */
|
|
2276
|
+
var nativeRaf = window.requestAnimationFrame && window.requestAnimationFrame.bind(window);
|
|
2277
|
+
var nativeCaf = window.cancelAnimationFrame && window.cancelAnimationFrame.bind(window);
|
|
2278
|
+
|
|
2279
|
+
if (nativeRaf) {
|
|
2280
|
+
var SHIM_BASE = 1000000000; // keep our ids clear of the native ones
|
|
2281
|
+
var nextRafId = SHIM_BASE;
|
|
2282
|
+
var rafQueue = new Map();
|
|
2283
|
+
|
|
2284
|
+
window.requestAnimationFrame = function(cb) {
|
|
2285
|
+
if (!active()) return nativeRaf(cb);
|
|
2286
|
+
var id = nextRafId++;
|
|
2287
|
+
rafQueue.set(id, cb);
|
|
2288
|
+
return id;
|
|
2289
|
+
};
|
|
2290
|
+
|
|
2291
|
+
window.cancelAnimationFrame = function(id) {
|
|
2292
|
+
if (id >= SHIM_BASE) { rafQueue.delete(id); return; }
|
|
2293
|
+
if (nativeCaf) nativeCaf(id);
|
|
2294
|
+
};
|
|
2295
|
+
|
|
2296
|
+
tickHandlers.push(function() {
|
|
2297
|
+
if (rafQueue.size === 0) return;
|
|
2298
|
+
var due = rafQueue;
|
|
2299
|
+
rafQueue = new Map();
|
|
2300
|
+
var stamp = performance.now();
|
|
2301
|
+
due.forEach(function(cb) {
|
|
2302
|
+
try { cb(stamp); } catch (e) {}
|
|
2303
|
+
});
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/* ---------- setTimeout / setInterval ---------- */
|
|
2308
|
+
var nativeSetTimeout = window.setTimeout.bind(window);
|
|
2309
|
+
var nativeClearTimeout = window.clearTimeout.bind(window);
|
|
2310
|
+
var nativeSetInterval = window.setInterval.bind(window);
|
|
2311
|
+
var nativeClearInterval = window.clearInterval.bind(window);
|
|
2312
|
+
|
|
2313
|
+
var TIMER_BASE = 2000000000;
|
|
2314
|
+
var nextTimerId = TIMER_BASE;
|
|
2315
|
+
var timers = new Map();
|
|
2316
|
+
|
|
2317
|
+
function schedule(fn, delay, args, repeating) {
|
|
2318
|
+
var wait = Math.max(0, Number(delay) || 0);
|
|
2319
|
+
var id = nextTimerId++;
|
|
2320
|
+
timers.set(id, { fn: fn, args: args, due: performance.now() + wait, every: repeating ? wait : null });
|
|
2321
|
+
return id;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
window.setTimeout = function(fn, delay) {
|
|
2325
|
+
var args = Array.prototype.slice.call(arguments, 2);
|
|
2326
|
+
if (!active() || typeof fn !== "function") return nativeSetTimeout.apply(window, arguments);
|
|
2327
|
+
return schedule(fn, delay, args, false);
|
|
2328
|
+
};
|
|
2329
|
+
|
|
2330
|
+
window.setInterval = function(fn, delay) {
|
|
2331
|
+
var args = Array.prototype.slice.call(arguments, 2);
|
|
2332
|
+
if (!active() || typeof fn !== "function") return nativeSetInterval.apply(window, arguments);
|
|
2333
|
+
return schedule(fn, delay, args, true);
|
|
2334
|
+
};
|
|
2335
|
+
|
|
2336
|
+
window.clearTimeout = function(id) {
|
|
2337
|
+
if (id >= TIMER_BASE) { timers.delete(id); return; }
|
|
2338
|
+
nativeClearTimeout(id);
|
|
2339
|
+
};
|
|
2340
|
+
|
|
2341
|
+
window.clearInterval = function(id) {
|
|
2342
|
+
if (id >= TIMER_BASE) { timers.delete(id); return; }
|
|
2343
|
+
nativeClearInterval(id);
|
|
2344
|
+
};
|
|
2345
|
+
|
|
2346
|
+
tickHandlers.push(function() {
|
|
2347
|
+
if (timers.size === 0) return;
|
|
2348
|
+
var now = performance.now();
|
|
2349
|
+
timers.forEach(function(timer, id) {
|
|
2350
|
+
if (now < timer.due) return;
|
|
2351
|
+
if (timer.every === null) timers.delete(id);
|
|
2352
|
+
else timer.due = now + timer.every;
|
|
2353
|
+
try { timer.fn.apply(window, timer.args); } catch (e) {}
|
|
2354
|
+
});
|
|
2355
|
+
});
|
|
2356
|
+
})();
|
|
2357
|
+
<\/script>`;
|
|
2358
|
+
|
|
2359
|
+
if (/<\/body>/i.test(html)) return html.replace(/<\/body>/i, `${script}</body>`);
|
|
2360
|
+
if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, `${script}</head>`);
|
|
2361
|
+
return `${html}${script}`;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
1671
2364
|
function injectAutoclickSupport(html) {
|
|
1672
2365
|
const clickScript = `
|
|
1673
2366
|
<script>
|
|
@@ -1741,6 +2434,164 @@
|
|
|
1741
2434
|
e.preventDefault();
|
|
1742
2435
|
openGameTab(file, { background: true });
|
|
1743
2436
|
});
|
|
2437
|
+
el.addEventListener("contextmenu", (e) => {
|
|
2438
|
+
e.preventDefault();
|
|
2439
|
+
openGameContextMenu(e, file);
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/* ---------------- right-click menu ---------------- */
|
|
2444
|
+
|
|
2445
|
+
let ctxMenu = null;
|
|
2446
|
+
|
|
2447
|
+
function hideGameContextMenu() {
|
|
2448
|
+
if (ctxMenu) ctxMenu.classList.remove("open");
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function ensureGameContextMenu() {
|
|
2452
|
+
if (ctxMenu) return ctxMenu;
|
|
2453
|
+
|
|
2454
|
+
ctxMenu = document.createElement("div");
|
|
2455
|
+
ctxMenu.id = "game-context-menu";
|
|
2456
|
+
ctxMenu.setAttribute("role", "menu");
|
|
2457
|
+
document.body.appendChild(ctxMenu);
|
|
2458
|
+
|
|
2459
|
+
// Any interaction outside the menu dismisses it.
|
|
2460
|
+
document.addEventListener("mousedown", (e) => {
|
|
2461
|
+
if (ctxMenu.classList.contains("open") && !ctxMenu.contains(e.target)) hideGameContextMenu();
|
|
2462
|
+
});
|
|
2463
|
+
document.addEventListener("keydown", (e) => {
|
|
2464
|
+
if (e.key === "Escape") hideGameContextMenu();
|
|
2465
|
+
});
|
|
2466
|
+
window.addEventListener("resize", hideGameContextMenu);
|
|
2467
|
+
window.addEventListener("blur", hideGameContextMenu);
|
|
2468
|
+
document.addEventListener("scroll", hideGameContextMenu, true);
|
|
2469
|
+
|
|
2470
|
+
return ctxMenu;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
function openGameContextMenu(e, file) {
|
|
2474
|
+
const menu = ensureGameContextMenu();
|
|
2475
|
+
menu.innerHTML = "";
|
|
2476
|
+
|
|
2477
|
+
const items = [
|
|
2478
|
+
["Open", () => openGameTab(file)],
|
|
2479
|
+
["Open Game in New Subtab", () => openGameTab(file, { background: true })],
|
|
2480
|
+
["Open Game in New Tab", () => openGameInNewWindow(file)],
|
|
2481
|
+
];
|
|
2482
|
+
|
|
2483
|
+
items.forEach(([label, action]) => {
|
|
2484
|
+
const item = document.createElement("button");
|
|
2485
|
+
item.type = "button";
|
|
2486
|
+
item.className = "ctx-item";
|
|
2487
|
+
item.setAttribute("role", "menuitem");
|
|
2488
|
+
item.textContent = label;
|
|
2489
|
+
item.onclick = () => { hideGameContextMenu(); action(); };
|
|
2490
|
+
menu.appendChild(item);
|
|
2491
|
+
});
|
|
2492
|
+
|
|
2493
|
+
// Show first so the box has a measurable size, then clamp on-screen.
|
|
2494
|
+
menu.classList.add("open");
|
|
2495
|
+
menu.style.left = "0px";
|
|
2496
|
+
menu.style.top = "0px";
|
|
2497
|
+
const rect = menu.getBoundingClientRect();
|
|
2498
|
+
const x = Math.min(e.clientX, window.innerWidth - rect.width - 8);
|
|
2499
|
+
const y = Math.min(e.clientY, window.innerHeight - rect.height - 8);
|
|
2500
|
+
menu.style.left = `${Math.max(8, x)}px`;
|
|
2501
|
+
menu.style.top = `${Math.max(8, y)}px`;
|
|
2502
|
+
|
|
2503
|
+
const first = menu.querySelector(".ctx-item");
|
|
2504
|
+
if (first) first.focus();
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
/* Plain fetch across the proxy list — the in-tab loader logs its progress
|
|
2508
|
+
into the tab, but a popup has nowhere to put that. */
|
|
2509
|
+
async function fetchGameSource(file) {
|
|
2510
|
+
const encoded = encodeURIComponent(normalizeFileName(file));
|
|
2511
|
+
for (const base of GAME_BASES) {
|
|
2512
|
+
try {
|
|
2513
|
+
const response = await fetch(`${base}${encoded}?t=${Date.now()}`);
|
|
2514
|
+
if (!response.ok) continue;
|
|
2515
|
+
const text = await response.text();
|
|
2516
|
+
const clean = text.trim().toLowerCase();
|
|
2517
|
+
if (clean === "404: not found" || clean === "not found" || clean.includes("404 page not found")) continue;
|
|
2518
|
+
return { text, base };
|
|
2519
|
+
} catch (e) { /* try the next proxy */ }
|
|
2520
|
+
}
|
|
2521
|
+
return null;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
/* Opens the game in a real browser tab on about:blank, wearing the home
|
|
2525
|
+
tab's title and favicon rather than the game's. */
|
|
2526
|
+
async function openGameInNewWindow(file) {
|
|
2527
|
+
const win = window.open("about:blank", "_blank");
|
|
2528
|
+
if (!win) {
|
|
2529
|
+
alert("The popup was blocked. Allow popups for this page to open games in a new tab.");
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
const title = displayNameFromFile(file);
|
|
2534
|
+
const icons = [...document.querySelectorAll("link[rel='icon']")].map(l => l.outerHTML).join("\n");
|
|
2535
|
+
|
|
2536
|
+
win.document.write(`<!DOCTYPE html>
|
|
2537
|
+
<html>
|
|
2538
|
+
<head>
|
|
2539
|
+
<meta charset="utf-8">
|
|
2540
|
+
<title>New tab</title>
|
|
2541
|
+
${icons}
|
|
2542
|
+
<style>
|
|
2543
|
+
html, body { margin: 0; height: 100%; background: linear-gradient(135deg, #2e3244 30%, #561a86 100%); }
|
|
2544
|
+
iframe { border: 0; display: block; width: 100%; height: 100%; }
|
|
2545
|
+
#loading { display: grid; place-items: center; height: 100%; color: #fff; font-family: 'Comic Sans MS', sans-serif; font-size: 20px; }
|
|
2546
|
+
</style>
|
|
2547
|
+
</head>
|
|
2548
|
+
<body><div id="loading">Loading ${sanitizeTitle(title)}...</div>
|
|
2549
|
+
<script>
|
|
2550
|
+
// Same trick as the main page: re-append the icon links so the browser
|
|
2551
|
+
// picks the favicon back up after the document is written.
|
|
2552
|
+
function refreshFavicon() {
|
|
2553
|
+
const existingIcons = document.querySelectorAll("link[rel='icon']");
|
|
2554
|
+
existingIcons.forEach(oldIcon => {
|
|
2555
|
+
const newIcon = oldIcon.cloneNode(true);
|
|
2556
|
+
oldIcon.remove();
|
|
2557
|
+
document.head.appendChild(newIcon);
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
setTimeout(refreshFavicon, 51);
|
|
2561
|
+
<\/script>
|
|
2562
|
+
</body>
|
|
2563
|
+
</html>`);
|
|
2564
|
+
win.document.close();
|
|
2565
|
+
|
|
2566
|
+
// document.write resets the title in some browsers; pin it afterwards.
|
|
2567
|
+
try { win.document.title = "New tab"; } catch (err) { }
|
|
2568
|
+
|
|
2569
|
+
recordRecent(file);
|
|
2570
|
+
|
|
2571
|
+
const source = await fetchGameSource(file);
|
|
2572
|
+
if (win.closed) return;
|
|
2573
|
+
|
|
2574
|
+
const loading = win.document.getElementById("loading");
|
|
2575
|
+
|
|
2576
|
+
if (!source) {
|
|
2577
|
+
if (loading) {
|
|
2578
|
+
loading.textContent = `Failed to load ${sanitizeTitle(title)}.`;
|
|
2579
|
+
loading.style.color = "#ff5555";
|
|
2580
|
+
}
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
let html = injectOffTabSupport(injectAutoclickSupport(injectMuteSupport(source.text, isMuted)), bgRunEnabled);
|
|
2585
|
+
html = injectBaseHref(html, source.base);
|
|
2586
|
+
|
|
2587
|
+
if (loading) loading.remove();
|
|
2588
|
+
const iframe = win.document.createElement("iframe");
|
|
2589
|
+
iframe.srcdoc = html;
|
|
2590
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-modals");
|
|
2591
|
+
iframe.referrerPolicy = "no-referrer";
|
|
2592
|
+
win.document.body.appendChild(iframe);
|
|
2593
|
+
|
|
2594
|
+
try { win.refreshFavicon && win.setTimeout(win.refreshFavicon, 51); } catch (err) { }
|
|
1744
2595
|
}
|
|
1745
2596
|
|
|
1746
2597
|
async function openGameTab(file, opts = {}) {
|
|
@@ -1821,7 +2672,7 @@
|
|
|
1821
2672
|
|
|
1822
2673
|
if (!text) throw new Error("All proxy servers failed, were skipped, or the file does not exist.");
|
|
1823
2674
|
|
|
1824
|
-
let html = injectAutoclickSupport(injectMuteSupport(text, isMuted));
|
|
2675
|
+
let html = injectOffTabSupport(injectAutoclickSupport(injectMuteSupport(text, isMuted)), bgRunEnabled);
|
|
1825
2676
|
html = injectBaseHref(html, successBase);
|
|
1826
2677
|
|
|
1827
2678
|
tab.panel.innerHTML = "";
|
|
@@ -1832,6 +2683,7 @@
|
|
|
1832
2683
|
iframe.referrerPolicy = "no-referrer";
|
|
1833
2684
|
applyMuteToIframe(iframe);
|
|
1834
2685
|
tab.panel.appendChild(iframe);
|
|
2686
|
+
restoreClickersForTab(id, tab.panel, iframe);
|
|
1835
2687
|
setTabStatus(id, isTabActive(id) ? null : "ready");
|
|
1836
2688
|
|
|
1837
2689
|
} catch (err) {
|
|
@@ -1845,14 +2697,119 @@
|
|
|
1845
2697
|
}
|
|
1846
2698
|
}
|
|
1847
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, "&")
|
|
2712
|
+
.replace(/</g, "<")
|
|
2713
|
+
.replace(/>/g, ">")
|
|
2714
|
+
.replace(/"/g, """);
|
|
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
|
+
|
|
1848
2802
|
function openHtmlPreviewTab(htmlCode) {
|
|
1849
2803
|
const id = "html-preview";
|
|
1850
|
-
const title = "HTML Code";
|
|
2804
|
+
const title = extractDocTitle(htmlCode) || "HTML Code";
|
|
1851
2805
|
|
|
1852
2806
|
const prepared = injectBaseHref(injectAutoclickSupport(injectMuteSupport(htmlCode, isMuted)), document.baseURI);
|
|
1853
2807
|
|
|
1854
2808
|
if (tabs.has(id)) {
|
|
1855
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);
|
|
1856
2813
|
existing.panel.innerHTML = "";
|
|
1857
2814
|
const iframe = document.createElement("iframe");
|
|
1858
2815
|
iframe.className = "game-frame";
|
|
@@ -1894,82 +2851,6 @@
|
|
|
1894
2851
|
activateTab(id);
|
|
1895
2852
|
}
|
|
1896
2853
|
|
|
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
2854
|
function openRunnerTab() {
|
|
1974
2855
|
const id = "runner";
|
|
1975
2856
|
const title = "HTML Runner";
|
|
@@ -2004,9 +2885,14 @@
|
|
|
2004
2885
|
</body>
|
|
2005
2886
|
</html></textarea>
|
|
2006
2887
|
<div class="runner-controls">
|
|
2007
|
-
<button id="runner-run-btn">Open in New
|
|
2888
|
+
<button id="runner-run-btn">Open in New Subtab</button>
|
|
2889
|
+
<button id="runner-newtab-btn">Open in New Tab</button>
|
|
2008
2890
|
<button id="runner-clear-btn">Clear</button>
|
|
2009
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>
|
|
2010
2896
|
</div>
|
|
2011
2897
|
</div>
|
|
2012
2898
|
`;
|
|
@@ -2021,12 +2907,18 @@
|
|
|
2021
2907
|
|
|
2022
2908
|
const code = panel.querySelector("#runner-code");
|
|
2023
2909
|
const runBtn = panel.querySelector("#runner-run-btn");
|
|
2910
|
+
const newTabBtn = panel.querySelector("#runner-newtab-btn");
|
|
2911
|
+
const cloakBox = panel.querySelector("#runner-cloak");
|
|
2024
2912
|
const clearBtn = panel.querySelector("#runner-clear-btn");
|
|
2025
2913
|
|
|
2026
2914
|
runBtn.onclick = () => {
|
|
2027
2915
|
openHtmlPreviewTab(code.value);
|
|
2028
2916
|
};
|
|
2029
2917
|
|
|
2918
|
+
newTabBtn.onclick = () => {
|
|
2919
|
+
openHtmlInNewWindow(code.value, cloakBox.checked);
|
|
2920
|
+
};
|
|
2921
|
+
|
|
2030
2922
|
clearBtn.onclick = () => {
|
|
2031
2923
|
code.value = "";
|
|
2032
2924
|
};
|
|
@@ -2052,6 +2944,7 @@
|
|
|
2052
2944
|
<textarea class="runner-textarea" placeholder="Paste your bookmarklet javascript: here..."></textarea>
|
|
2053
2945
|
<div class="runner-controls">
|
|
2054
2946
|
<button class="run-btn">Run Bookmarklet</button>
|
|
2947
|
+
<button class="clear-btn">Clear</button>
|
|
2055
2948
|
</div>
|
|
2056
2949
|
</div>
|
|
2057
2950
|
</div>
|
|
@@ -2059,6 +2952,11 @@
|
|
|
2059
2952
|
|
|
2060
2953
|
const textarea = tab.panel.querySelector(".runner-textarea");
|
|
2061
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
|
+
});
|
|
2062
2960
|
|
|
2063
2961
|
runBtn.addEventListener("click", () => {
|
|
2064
2962
|
let code = textarea.value.trim();
|
|
@@ -2134,13 +3032,44 @@
|
|
|
2134
3032
|
const container = document.getElementById("sections-container");
|
|
2135
3033
|
const firstRealSection = container.firstElementChild;
|
|
2136
3034
|
container.insertBefore(section, firstRealSection);
|
|
3035
|
+
|
|
3036
|
+
filterGames();
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
function renderRecentSection() {
|
|
3040
|
+
const container = document.getElementById("sections-container");
|
|
3041
|
+
if (!container) return;
|
|
3042
|
+
|
|
3043
|
+
const existing = document.getElementById("section-recent");
|
|
3044
|
+
if (existing) existing.remove();
|
|
3045
|
+
|
|
3046
|
+
const recentSidebarBtn = document.getElementById("sidebar-recent");
|
|
3047
|
+
if (recentSidebarBtn) recentSidebarBtn.style.opacity = recent.length > 0 ? "1" : "0.3";
|
|
3048
|
+
|
|
3049
|
+
if (recent.length === 0) return;
|
|
3050
|
+
|
|
3051
|
+
const section = document.createElement("div");
|
|
3052
|
+
section.className = "letter-section"; section.id = "section-recent";
|
|
3053
|
+
|
|
3054
|
+
const header = document.createElement("div");
|
|
3055
|
+
header.className = "letter-header"; header.textContent = "Recently Played"; section.appendChild(header);
|
|
3056
|
+
|
|
3057
|
+
const buttonsContainer = document.createElement("div");
|
|
3058
|
+
buttonsContainer.className = "buttons-container";
|
|
3059
|
+
recent.forEach(file => buttonsContainer.appendChild(makeGameRow(file)));
|
|
3060
|
+
section.appendChild(buttonsContainer);
|
|
3061
|
+
|
|
3062
|
+
const fav = document.getElementById("section-fav");
|
|
3063
|
+
container.insertBefore(section, fav ? fav.nextElementSibling : container.firstElementChild);
|
|
3064
|
+
|
|
3065
|
+
filterGames();
|
|
2137
3066
|
}
|
|
2138
3067
|
|
|
2139
3068
|
function generateSidebar(allChars, filesByChar) {
|
|
2140
3069
|
const sidebar = document.getElementById("sidebar"); sidebar.innerHTML = "";
|
|
2141
3070
|
|
|
2142
3071
|
const searchBtn = document.createElement("button");
|
|
2143
|
-
searchBtn.className = "sidebar-btn"; searchBtn.textContent = "
|
|
3072
|
+
searchBtn.className = "sidebar-btn"; searchBtn.textContent = "search";
|
|
2144
3073
|
searchBtn.onclick = () => document.getElementById("gameSearch").scrollIntoView({ behavior: "smooth", block: "end" });
|
|
2145
3074
|
sidebar.appendChild(searchBtn);
|
|
2146
3075
|
|
|
@@ -2153,6 +3082,15 @@
|
|
|
2153
3082
|
};
|
|
2154
3083
|
sidebar.appendChild(favBtn);
|
|
2155
3084
|
|
|
3085
|
+
const recentBtn = document.createElement("button");
|
|
3086
|
+
recentBtn.className = "sidebar-btn"; recentBtn.id = "sidebar-recent"; recentBtn.textContent = "recent";
|
|
3087
|
+
recentBtn.style.opacity = recent.length > 0 ? "1" : "0.3";
|
|
3088
|
+
recentBtn.onclick = () => {
|
|
3089
|
+
const section = document.getElementById("section-recent");
|
|
3090
|
+
if (section) section.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
3091
|
+
};
|
|
3092
|
+
sidebar.appendChild(recentBtn);
|
|
3093
|
+
|
|
2156
3094
|
allChars.forEach((char) => {
|
|
2157
3095
|
if (char === "Search") return;
|
|
2158
3096
|
const btn = document.createElement("button");
|
|
@@ -2200,11 +3138,12 @@
|
|
|
2200
3138
|
section.appendChild(buttonsContainer); container.appendChild(section);
|
|
2201
3139
|
});
|
|
2202
3140
|
|
|
2203
|
-
generateSidebar(sidebarChars, filesByChar); renderFavoritesSection();
|
|
3141
|
+
generateSidebar(sidebarChars, filesByChar); renderFavoritesSection(); renderRecentSection();
|
|
2204
3142
|
}
|
|
2205
3143
|
|
|
2206
3144
|
function filterGames() {
|
|
2207
3145
|
const input = document.getElementById("gameSearch");
|
|
3146
|
+
if (!input) return;
|
|
2208
3147
|
const filter = input.value.toLowerCase();
|
|
2209
3148
|
const sections = document.querySelectorAll(".letter-section");
|
|
2210
3149
|
|
|
@@ -2226,7 +3165,6 @@
|
|
|
2226
3165
|
}
|
|
2227
3166
|
|
|
2228
3167
|
document.getElementById("open-runner-btn").addEventListener("click", openRunnerTab);
|
|
2229
|
-
document.getElementById("open-recent-btn").addEventListener("click", openRecentGamesTab);
|
|
2230
3168
|
|
|
2231
3169
|
async function fetchAndInjectProxy(targetUrl, tab, title, id) {
|
|
2232
3170
|
const logArea = tab.panel.querySelector(`#logs-${id}`);
|
|
@@ -2361,6 +3299,13 @@
|
|
|
2361
3299
|
|
|
2362
3300
|
document.getElementById("open-form-btn").addEventListener("click", openFormTab);
|
|
2363
3301
|
|
|
3302
|
+
// Ask the browser to show its native "Leave site?" prompt on close/reload.
|
|
3303
|
+
window.addEventListener("beforeunload", (e) => {
|
|
3304
|
+
e.preventDefault();
|
|
3305
|
+
e.returnValue = "";
|
|
3306
|
+
return "";
|
|
3307
|
+
});
|
|
3308
|
+
|
|
2364
3309
|
function initWhenReady() {
|
|
2365
3310
|
if (typeof files === "undefined") {
|
|
2366
3311
|
setTimeout(initWhenReady, 50); return;
|