pinokiod 3.41.0 → 3.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/kernel/api/browser/index.js +3 -1
  2. package/kernel/api/cloudflare/index.js +3 -3
  3. package/kernel/api/index.js +187 -51
  4. package/kernel/api/loading/index.js +15 -0
  5. package/kernel/api/process/index.js +7 -0
  6. package/kernel/api/shell/index.js +0 -2
  7. package/kernel/bin/browserless.js +22 -0
  8. package/kernel/bin/caddy.js +36 -4
  9. package/kernel/bin/index.js +4 -1
  10. package/kernel/bin/setup.js +38 -5
  11. package/kernel/connect/backend.js +110 -0
  12. package/kernel/connect/config.js +171 -0
  13. package/kernel/connect/index.js +18 -7
  14. package/kernel/connect/providers/huggingface/index.js +98 -0
  15. package/kernel/connect/providers/x/index.js +0 -1
  16. package/kernel/environment.js +91 -19
  17. package/kernel/git.js +46 -3
  18. package/kernel/index.js +119 -39
  19. package/kernel/peer.js +40 -5
  20. package/kernel/plugin.js +3 -2
  21. package/kernel/procs.js +27 -20
  22. package/kernel/prototype.js +30 -16
  23. package/kernel/router/common.js +1 -1
  24. package/kernel/router/connector.js +1 -3
  25. package/kernel/router/index.js +38 -4
  26. package/kernel/router/localhost_home_router.js +5 -1
  27. package/kernel/router/localhost_port_router.js +27 -1
  28. package/kernel/router/localhost_static_router.js +93 -0
  29. package/kernel/router/localhost_variable_router.js +14 -9
  30. package/kernel/router/peer_peer_router.js +3 -0
  31. package/kernel/router/peer_static_router.js +43 -0
  32. package/kernel/router/peer_variable_router.js +15 -14
  33. package/kernel/router/processor.js +26 -1
  34. package/kernel/router/rewriter.js +59 -0
  35. package/kernel/scripts/git/commit +11 -1
  36. package/kernel/shell.js +8 -3
  37. package/kernel/util.js +65 -6
  38. package/package.json +2 -1
  39. package/server/index.js +1037 -964
  40. package/server/public/common.js +382 -1
  41. package/server/public/fscreator.js +0 -1
  42. package/server/public/loading.js +17 -0
  43. package/server/public/notifyinput.js +0 -1
  44. package/server/public/opener.js +4 -2
  45. package/server/public/style.css +311 -11
  46. package/server/socket.js +7 -1
  47. package/server/views/app.ejs +1747 -351
  48. package/server/views/columns.ejs +338 -0
  49. package/server/views/connect/huggingface.ejs +353 -0
  50. package/server/views/connect/index.ejs +410 -0
  51. package/server/views/connect/x.ejs +43 -9
  52. package/server/views/connect.ejs +709 -49
  53. package/server/views/container.ejs +357 -0
  54. package/server/views/d.ejs +251 -62
  55. package/server/views/download.ejs +54 -10
  56. package/server/views/editor.ejs +11 -0
  57. package/server/views/explore.ejs +40 -15
  58. package/server/views/file_explorer.ejs +25 -246
  59. package/server/views/form.ejs +44 -1
  60. package/server/views/frame.ejs +39 -1
  61. package/server/views/github.ejs +48 -11
  62. package/server/views/help.ejs +48 -7
  63. package/server/views/index.ejs +119 -58
  64. package/server/views/index2.ejs +3 -4
  65. package/server/views/init/index.ejs +651 -197
  66. package/server/views/install.ejs +1 -1
  67. package/server/views/mini.ejs +47 -18
  68. package/server/views/net.ejs +199 -67
  69. package/server/views/network.ejs +220 -94
  70. package/server/views/network2.ejs +3 -4
  71. package/server/views/old_network.ejs +3 -3
  72. package/server/views/prototype/index.ejs +48 -11
  73. package/server/views/review.ejs +1005 -0
  74. package/server/views/rows.ejs +341 -0
  75. package/server/views/screenshots.ejs +1020 -0
  76. package/server/views/settings.ejs +160 -23
  77. package/server/views/setup.ejs +49 -7
  78. package/server/views/setup_home.ejs +43 -10
  79. package/server/views/shell.ejs +7 -1
  80. package/server/views/start.ejs +14 -9
  81. package/server/views/terminal.ejs +13 -2
  82. package/server/views/tools.ejs +1015 -0
@@ -0,0 +1,1020 @@
1
+ <html>
2
+ <head>
3
+ <meta charset="UTF-8">
4
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
5
+ <link href="/xterm.min.css" rel="stylesheet" />
6
+ <link href="/css/fontawesome.min.css" rel="stylesheet">
7
+ <link href="/css/solid.min.css" rel="stylesheet">
8
+ <link href="/css/regular.min.css" rel="stylesheet">
9
+ <link href="/css/brands.min.css" rel="stylesheet">
10
+ <link href="/markdown.css" rel="stylesheet"/>
11
+ <link href="/noty.css" rel="stylesheet"/>
12
+ <link href="/style.css" rel="stylesheet"/>
13
+ <% if (agent === "electron") { %>
14
+ <link href="/electron.css" rel="stylesheet"/>
15
+ <% } %>
16
+ <style>
17
+ .line2 {
18
+ display: flex;
19
+ align-items: center;
20
+ cursor: pointer;
21
+ background: rgba(0,0,100,0.04);
22
+ }
23
+ .line2 a {
24
+ text-decoration: none;
25
+ color: black;
26
+ }
27
+ .status {
28
+ padding: 10px;
29
+ margin: 10px;
30
+ border-radius: 10px;
31
+ }
32
+ .status.offline {
33
+ background: silver;
34
+ }
35
+ .status.online {
36
+ background: yellowgreen;
37
+ }
38
+ .switch {
39
+ padding: 10px;
40
+ margin: 10px 0;
41
+ }
42
+ .switch[data-online=true] {
43
+ color: yellowgreen;
44
+ }
45
+ .button {
46
+ padding: 10px;
47
+ }
48
+ .on, .off {
49
+ display: flex;
50
+ align-items: center;
51
+ }
52
+ /*
53
+ .btn {
54
+ margin-right: 5px;
55
+ font-weight: normal;
56
+ padding: 5px 15px;
57
+ min-width: 100px;
58
+ text-align: center;
59
+ }
60
+ */
61
+ .items {
62
+ max-width: 600px;
63
+ margin: 50px auto;
64
+ }
65
+ body.dark .item {
66
+ border-left: 5px solid rgba(255,255,255,0.06);
67
+ }
68
+ .item {
69
+ margin: 0;
70
+ /*
71
+ border-top: 1px solid rgba(255,255,255,0.1);
72
+ border-bottom: 1px solid rgba(255,255,255,0.1);
73
+ */
74
+ padding: 10px;
75
+ border-left: 5px solid rgba(0,0,0,0.06);
76
+ }
77
+ .titleview {
78
+ margin: 0;
79
+ padding: 25px;
80
+ max-width: 600px;
81
+ }
82
+ .titleview h1 {
83
+ word-break: break-word;
84
+ font-size: 40px;
85
+ margin-bottom: 10px;
86
+ }
87
+ .item > .d {
88
+ padding-bottom: 10px;
89
+ }
90
+ .item label {
91
+ display: block;
92
+ text-transform: capitalize;
93
+ font-size: 20px;
94
+ font-weight: bold;
95
+ padding-bottom: 5px;
96
+ }
97
+ .item .explanation {
98
+ padding-top: 5px;
99
+ color: #bf411d;
100
+ font-size: 12px;
101
+ }
102
+ .item input[type=text] {
103
+ background: whitesmoke;
104
+ }
105
+ .item select {
106
+ background: whitesmoke;
107
+ }
108
+ /*
109
+ body.dark .item input[type=text] {
110
+ background: rgba(255,255,255,0.1);
111
+ color: white;
112
+ }
113
+ body.dark .item select {
114
+ color: white;
115
+ background: rgba(255,255,255,0.1);
116
+ }
117
+ */
118
+ .item input[type=text] {
119
+ padding: 10px;
120
+ flex-grow: 1;
121
+ border: none;
122
+ /*
123
+ background: rgba(0,0,0,0.05);
124
+ */
125
+ width: 100%;
126
+ }
127
+ .item select {
128
+ /*
129
+ -webkit-appearance: none;
130
+ -moz-appearance: none;
131
+ appearance: none;
132
+ */
133
+
134
+ /*
135
+ background: rgba(0,0,0,0.05);
136
+ */
137
+ padding: 10px;
138
+ box-sizing: border-box;
139
+ width: 100%;
140
+ border: none;
141
+ }
142
+ .item img {
143
+ width: 100px;
144
+ }
145
+ .item .title {
146
+ text-decoration: none;
147
+ color: royalblue;
148
+ }
149
+ .item .col {
150
+ padding: 10px;
151
+ }
152
+ .item .col > * {
153
+ margin: 5px 0;
154
+ }
155
+ .item .stat {
156
+ color: rgba(0,0,0,0.8);
157
+ display: flex;
158
+ }
159
+ .item .stat > * {
160
+ margin-right: 15px;
161
+ }
162
+ .timestamp {
163
+ color: rgba(0,0,0,0.5);
164
+ }
165
+ body.dark .loading {
166
+ background: rgba(255,255,255,0.06);
167
+ }
168
+ .loading {
169
+ padding: 20px;
170
+ text-align: center;
171
+ background: rgba(0,0,0,0.06);
172
+ }
173
+ body.dark .btn {
174
+ color: white;
175
+ background: rgba(255,255,255,0.1);
176
+ }
177
+ body.light {
178
+ /*
179
+ background: var(--light-nav-bg);
180
+ */
181
+ }
182
+ /*
183
+ form {
184
+ text-align: center;
185
+ }
186
+ body.dark header .btn2 {
187
+ color: var(--light-link-color);
188
+ }
189
+ header .btn2 {
190
+ color: var(--light-color);
191
+ }
192
+ */
193
+ /*
194
+ body.dark header .home {
195
+ color: var(--light-link-color);
196
+ }
197
+ */
198
+ header .home {
199
+ color: var(--light-color);
200
+ padding: 10px;
201
+ }
202
+ body.dark hr {
203
+ background: white;
204
+ }
205
+ hr {
206
+ opacity: 0.03;
207
+ background: black;
208
+ margin: 30px 0;
209
+ height: 1px;
210
+ border: none;
211
+ }
212
+ .reset-cache-loading {
213
+ padding: 10px;
214
+ background: rgba(0,0,0,0.1);
215
+ margin-bottom: 10px;
216
+ text-align: center;
217
+ }
218
+ .reset-bin-loading {
219
+ padding: 10px;
220
+ background: rgba(0,0,0,0.1);
221
+ margin-bottom: 10px;
222
+ text-align: center;
223
+ }
224
+ #proxy {
225
+ text-decoration: underline;
226
+ margin-left: 10px;
227
+ display: inline-block;
228
+ cursor: pointer;
229
+ }
230
+ body.dark .keys pre {
231
+ background: rgba(255,255,255,0.02) !important;
232
+ }
233
+ .keys pre {
234
+ padding: 20px;
235
+ margin: 20px 0;
236
+ background: rgba(0,0,100,0.04) !important;
237
+ }
238
+ .swal2-actions {
239
+ justify-content: center !important;
240
+ }
241
+ .swal2-title {
242
+ text-align: center !important;
243
+ }
244
+ main {
245
+ display: flex;
246
+ }
247
+ aside {
248
+ width: 200px;
249
+ display: block;
250
+ flex-shrink: 0;
251
+ }
252
+ aside .tab i {
253
+ width: 20px;
254
+ text-align: center;
255
+ }
256
+ body.dark aside .tab {
257
+ color: white;
258
+ }
259
+ body.dark aside .tab:hover, aside .tab:hover {
260
+ color: royalblue !important;
261
+ opacity: 1;
262
+ }
263
+ aside .tab {
264
+ display: flex;
265
+ align-items: center;
266
+ gap: 5px;
267
+ color: black;
268
+ text-decoration: none;
269
+ padding: 10px;
270
+ font-size: 12px;
271
+ opacity: 0.5;
272
+ border-left: 10px solid transparent;
273
+ }
274
+ body.dark aside .tab.selected {
275
+ color: white;
276
+ }
277
+ aside .selected {
278
+ font-weight: bold;
279
+ opacity: 1;
280
+ }
281
+ @media only screen and (max-width: 600px) {
282
+ aside {
283
+ width: unset;
284
+ flex-shrink: unset;
285
+ }
286
+ aside {
287
+ padding: 0 10px;
288
+ }
289
+ aside .tab i {
290
+ width: 100%;
291
+ }
292
+ aside .tab .caption {
293
+ display: none;
294
+ }
295
+ aside .tab {
296
+ margin: 0;
297
+ padding: 10px;
298
+ border-left: none;
299
+ }
300
+ aside .btn-tab {
301
+ flex-direction: column;
302
+ padding: 10px 0;
303
+ }
304
+ aside .btn-tab .btn {
305
+ display: flex;
306
+ justify-content: center;
307
+ }
308
+ aside .btn-tab .btn .caption {
309
+ display: none;
310
+ }
311
+ header .flexible {
312
+ min-width: unset;
313
+ }
314
+ }
315
+ @media only screen and (max-width: 480px) {
316
+ .btn2 {
317
+ padding: 5px;
318
+ font-size: 11px;
319
+ }
320
+ .nav-btns {
321
+ flex-grow: 1;
322
+ justify-content: center;
323
+ padding: 0;
324
+ }
325
+ }
326
+
327
+ /* Gallery Styles */
328
+ .gallery-container {
329
+ padding: 20px;
330
+ max-width: 1200px;
331
+ margin: 0 auto;
332
+ }
333
+
334
+ .gallery-header {
335
+ display: flex;
336
+ justify-content: space-between;
337
+ align-items: center;
338
+ margin-bottom: 20px;
339
+ padding-bottom: 10px;
340
+ border-bottom: 2px solid rgba(0,0,0,0.1);
341
+ }
342
+
343
+ body.dark .gallery-header {
344
+ border-bottom-color: rgba(255,255,255,0.1);
345
+ }
346
+
347
+ .gallery-header h2 {
348
+ margin: 0;
349
+ font-size: 28px;
350
+ }
351
+
352
+ .gallery-stats {
353
+ font-size: 14px;
354
+ color: rgba(0,0,0,0.6);
355
+ }
356
+
357
+ body.dark .gallery-stats {
358
+ color: rgba(255,255,255,0.6);
359
+ }
360
+
361
+ .gallery-grid {
362
+ display: grid;
363
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
364
+ gap: 20px;
365
+ padding: 20px 0;
366
+ }
367
+
368
+ .gallery-item {
369
+ position: relative;
370
+ background: rgba(0,0,0,0.03);
371
+ border-radius: 8px;
372
+ overflow: hidden;
373
+ cursor: pointer;
374
+ transition: transform 0.2s, box-shadow 0.2s;
375
+ border: 1px solid rgba(0,0,0,0.1);
376
+ }
377
+
378
+ .gallery-item-actions {
379
+ position: absolute;
380
+ top: 8px;
381
+ right: 8px;
382
+ display: flex;
383
+ gap: 4px;
384
+ opacity: 0;
385
+ transition: opacity 0.2s;
386
+ }
387
+
388
+ .gallery-item:hover .gallery-item-actions {
389
+ opacity: 1;
390
+ }
391
+
392
+ .gallery-item-action-btn {
393
+ width: 28px;
394
+ height: 28px;
395
+ border-radius: 50%;
396
+ border: none;
397
+ cursor: pointer;
398
+ display: flex;
399
+ align-items: center;
400
+ justify-content: center;
401
+ font-size: 12px;
402
+ transition: all 0.2s;
403
+ backdrop-filter: blur(8px);
404
+ }
405
+
406
+ .gallery-item-delete-btn {
407
+ background: rgba(220, 53, 69, 0.9);
408
+ color: white;
409
+ }
410
+
411
+ .gallery-item-delete-btn:hover {
412
+ background: rgba(220, 53, 69, 1);
413
+ transform: scale(1.1);
414
+ }
415
+
416
+ .gallery-item.deleting {
417
+ opacity: 0.5;
418
+ pointer-events: none;
419
+ }
420
+
421
+ .gallery-item.deleting::after {
422
+ content: '';
423
+ position: absolute;
424
+ top: 0;
425
+ left: 0;
426
+ right: 0;
427
+ bottom: 0;
428
+ background: rgba(0,0,0,0.7);
429
+ display: flex;
430
+ align-items: center;
431
+ justify-content: center;
432
+ color: white;
433
+ font-size: 14px;
434
+ }
435
+
436
+ .gallery-item.deleting::after {
437
+ content: 'Deleting...';
438
+ }
439
+
440
+ body.dark .gallery-item {
441
+ background: rgba(255,255,255,0.03);
442
+ border-color: rgba(255,255,255,0.1);
443
+ }
444
+
445
+ .gallery-item:hover {
446
+ transform: translateY(-2px);
447
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
448
+ }
449
+
450
+ .gallery-item img {
451
+ width: 100%;
452
+ height: 200px;
453
+ object-fit: cover;
454
+ display: block;
455
+ transition: opacity 0.2s;
456
+ }
457
+
458
+ .gallery-item img.loading {
459
+ opacity: 0.5;
460
+ }
461
+
462
+ .gallery-item-info {
463
+ padding: 10px;
464
+ text-align: center;
465
+ }
466
+
467
+ .gallery-item-filename {
468
+ font-size: 12px;
469
+ color: rgba(0,0,0,0.8);
470
+ margin-bottom: 5px;
471
+ word-break: break-all;
472
+ }
473
+
474
+ body.dark .gallery-item-filename {
475
+ color: rgba(255,255,255,0.8);
476
+ }
477
+
478
+ .gallery-item-timestamp {
479
+ font-size: 11px;
480
+ color: rgba(0,0,0,0.5);
481
+ }
482
+
483
+ body.dark .gallery-item-timestamp {
484
+ color: rgba(255,255,255,0.5);
485
+ }
486
+
487
+ .gallery-error {
488
+ text-align: center;
489
+ padding: 40px 20px;
490
+ color: #bf411d;
491
+ }
492
+
493
+ .gallery-error .btn {
494
+ margin-top: 10px;
495
+ padding: 8px 16px;
496
+ background: royalblue;
497
+ color: white;
498
+ border: none;
499
+ border-radius: 4px;
500
+ cursor: pointer;
501
+ font-size: 14px;
502
+ }
503
+
504
+ .gallery-error .btn:hover {
505
+ background: #4169e1;
506
+ }
507
+
508
+ /* Image modal/lightbox styles */
509
+ .image-modal {
510
+ position: fixed;
511
+ top: 0;
512
+ left: 0;
513
+ width: 100%;
514
+ height: 100%;
515
+ background: rgba(0,0,0,0.9);
516
+ display: flex;
517
+ justify-content: center;
518
+ align-items: center;
519
+ z-index: 10000;
520
+ cursor: pointer;
521
+ }
522
+
523
+ .image-modal img {
524
+ max-width: 90%;
525
+ max-height: 90%;
526
+ object-fit: contain;
527
+ border-radius: 4px;
528
+ }
529
+
530
+ .image-modal-close {
531
+ position: absolute;
532
+ top: 20px;
533
+ right: 20px;
534
+ background: rgba(255,255,255,0.2);
535
+ color: white;
536
+ border: none;
537
+ width: 40px;
538
+ height: 40px;
539
+ border-radius: 50%;
540
+ cursor: pointer;
541
+ font-size: 18px;
542
+ display: flex;
543
+ align-items: center;
544
+ justify-content: center;
545
+ }
546
+
547
+ @media only screen and (max-width: 800px) {
548
+ body {
549
+ display: flex !important;
550
+ flex-direction: row !important;
551
+ }
552
+ }
553
+ @media only screen and (max-width: 768px) {
554
+ .gallery-grid {
555
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
556
+ gap: 15px;
557
+ }
558
+
559
+ .gallery-header {
560
+ flex-direction: column;
561
+ align-items: flex-start;
562
+ gap: 10px;
563
+ }
564
+ }
565
+
566
+ @media only screen and (max-width: 600px) {
567
+ aside {
568
+ width: unset;
569
+ flex-shrink: unset;
570
+ }
571
+ aside {
572
+ padding: 0 10px;
573
+ }
574
+ aside .tab i {
575
+ width: 100%;
576
+ }
577
+ aside .tab .caption {
578
+ display: none;
579
+ }
580
+ aside .tab {
581
+ margin: 0;
582
+ padding: 10px;
583
+ border-left: none;
584
+ }
585
+ aside .btn-tab {
586
+ flex-direction: column;
587
+ padding: 10px 0;
588
+ }
589
+ aside .btn-tab .btn {
590
+ display: flex;
591
+ justify-content: center;
592
+ }
593
+ aside .btn-tab .btn .caption {
594
+ display: none;
595
+ }
596
+ header .flexible {
597
+ min-width: unset;
598
+ }
599
+ }
600
+
601
+
602
+ @media only screen and (max-width: 480px) {
603
+ .gallery-grid {
604
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
605
+ gap: 10px;
606
+ }
607
+
608
+ .gallery-container {
609
+ padding: 10px;
610
+ }
611
+ }
612
+ </style>
613
+ <script src="/popper.min.js"></script>
614
+ <script src="/tippy-bundle.umd.min.js"></script>
615
+ <script src="/hotkeys.min.js"></script>
616
+ <script src="/sweetalert2.js"></script>
617
+ <script src="/noty.js"></script>
618
+ <script src="/notyq.js"></script>
619
+ <script src="/xterm.js"></script>
620
+ <script src="/xterm-addon-fit.js"></script>
621
+ <script src="/xterm-addon-web-links.js"></script>
622
+ <script src="/xterm-theme.js"></script>
623
+ <script src="/install.js"></script>
624
+ <script src="/timeago.min.js"></script>
625
+ <script src="/common.js"></script>
626
+ <script src="/opener.js"></script>
627
+ <script src="/nav.js"></script>
628
+ <script src="/report.js"></script>
629
+ </head>
630
+ <body class='<%=theme%>' data-agent="<%=agent%>">
631
+ <!--
632
+ <nav>
633
+ <a class='logo' href="/">dal</a>
634
+ </nav>
635
+ -->
636
+ <header class='navheader grabbable'>
637
+ <h1>
638
+ <a class='home' href="/"><img class='icon' src="/pinokio-black.png"></a>
639
+ <button class='btn2' id='back' data-tippy-content="back"><div><i class="fa-solid fa-chevron-left"></i></div></button>
640
+ <button class='btn2' id='forward' data-tippy-content="forward"><div><i class="fa-solid fa-chevron-right"></i></div></button>
641
+ <button class='btn2' id='refresh-page' data-tippy-content="refresh"><div><i class="fa-solid fa-rotate-right"></i></div></button>
642
+ <button class='btn2' id='screenshot' data-tippy-content="take a screenshot"><i class="fa-solid fa-camera"></i></button>
643
+ <form class='urlbar'><input type='url' placeholder='enter a local url'></form>
644
+ <a class='btn2' href="/columns" data-tippy-content="split into 2 columns">
645
+ <div><i class="fa-solid fa-table-columns"></i></div>
646
+ </a>
647
+ <a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
648
+ <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
649
+ </a>
650
+ <div class="dropdown btn2">
651
+ <button class='btn2' id='window-management'>
652
+ <div><i class="fa-solid fa-plus"></i></div>
653
+ </button>
654
+ <div class="dropdown-content" id="dropdown-content">
655
+ <button class='btn2' id='clone-win' data-tippy-content="clone this window">
656
+ <div><i class="fa-solid fa-clone"></i><div>clone this window</div></div>
657
+ </button>
658
+ <button id='new-window' data-tippy-content="open a new window" title='open a new window' class='btn2' data-agent="<%=agent%>">
659
+ <div><i class="fa-solid fa-plus"></i><div>new window</div></div>
660
+ </button>
661
+ </div>
662
+ </div>
663
+ <button class='btn2 hidden' id='close-window' data-tippy-content='close this section'>
664
+ <div><i class="fa-solid fa-xmark"></i></div>
665
+ </button>
666
+ </h1>
667
+ </header>
668
+ <main>
669
+ <div class='container'>
670
+ <div id="gallery-loading" class="loading">Loading screenshots...</div>
671
+ <div id="gallery-error" class="gallery-error" style="display: none;">
672
+ <p>Failed to load screenshots. <button id="retry-btn" class="btn">Retry</button></p>
673
+ </div>
674
+ <div id="gallery-container" class="gallery-container" style="display: none;">
675
+ <div class="gallery-header">
676
+ <h2>Screenshots</h2>
677
+ <div class="gallery-stats">
678
+ <span id="image-count">0 images</span>
679
+ </div>
680
+ </div>
681
+ <div id="gallery-grid" class="gallery-grid">
682
+ <!-- Images will be populated here -->
683
+ </div>
684
+ </div>
685
+ </div>
686
+ <aside>
687
+ <div class='btn-tab'>
688
+ <a href="/init" class='btn'><i class="fa-solid fa-plus"></i><div class='caption'>Create</div></a>
689
+ <a class='btn' id='explore' href="/?mode=explore"><i class="fa-solid fa-globe"></i><div class='caption'>Discover</div></a>
690
+ </div>
691
+ <a href="/" class='tab'><i class='fas fa-laptop-code'></i><div class='caption'>This machine</div></a>
692
+ <a href="/network" class='tab'><i class="fa-solid fa-wifi"></i><div class='caption'>Local network</div></a>
693
+ <% if (list.length > 0) { %>
694
+ <% let brands = { win32: "windows", darwin: "apple", linux: "Linux" } %>
695
+ <% list.forEach(({ host, name, platform, processes }, index) => { %>
696
+ <a href="/net/<%=name%>" class='submenu tab'><i class="fa-brands fa-<%=brands[platform]%>"></i><div class='caption'><%=name%> (<%=current_host === host ? 'this machine' : host%>)</div></a>
697
+ <% }) %>
698
+ <% } %>
699
+ <a href="/connect" class='tab'><i class="fa-solid fa-plug"></i><div class='caption'>Login</div></a>
700
+ <a class='tab' href="<%=portal%>" target="_blank"><i class="fa-solid fa-question"></i><div class='caption'>Help</div></a>
701
+ <a class='tab' id='genlog'><i class="fa-solid fa-laptop-code"></i><div class='caption'>Logs</div></a>
702
+ <a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
703
+ <a class='tab selected' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
704
+ <a class='tab' href="/?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
705
+ </aside>
706
+ </main>
707
+ <script>
708
+ // Screenshot Gallery JavaScript
709
+ class ScreenshotGallery {
710
+ constructor() {
711
+ this.loadingEl = document.getElementById('gallery-loading')
712
+ this.errorEl = document.getElementById('gallery-error')
713
+ this.containerEl = document.getElementById('gallery-container')
714
+ this.gridEl = document.getElementById('gallery-grid')
715
+ this.imageCountEl = document.getElementById('image-count')
716
+ this.retryBtn = document.getElementById('retry-btn')
717
+
718
+ this.init()
719
+ }
720
+
721
+ init() {
722
+ this.loadScreenshots()
723
+ this.retryBtn.addEventListener('click', () => this.loadScreenshots())
724
+
725
+ // Handle escape key to close modal
726
+ document.addEventListener('keydown', (e) => {
727
+ if (e.key === 'Escape') {
728
+ this.closeModal()
729
+ }
730
+ })
731
+ }
732
+
733
+ async loadScreenshots() {
734
+ try {
735
+ this.showLoading()
736
+
737
+ const response = await fetch('/snapshots')
738
+ if (!response.ok) {
739
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
740
+ }
741
+
742
+ const data = await response.json()
743
+ this.displayScreenshots(data.files || [])
744
+
745
+ } catch (error) {
746
+ console.error('Failed to load screenshots:', error)
747
+ this.showError(error.message)
748
+ }
749
+ }
750
+
751
+ showLoading() {
752
+ this.loadingEl.style.display = 'block'
753
+ this.errorEl.style.display = 'none'
754
+ this.containerEl.style.display = 'none'
755
+ }
756
+
757
+ showError(message) {
758
+ this.loadingEl.style.display = 'none'
759
+ this.errorEl.style.display = 'block'
760
+ this.containerEl.style.display = 'none'
761
+
762
+ const errorMsg = this.errorEl.querySelector('p')
763
+ errorMsg.innerHTML = `Failed to load screenshots: ${message}. <button id="retry-btn" class="btn">Retry</button>`
764
+
765
+ // Re-attach event listener to new retry button
766
+ const newRetryBtn = document.getElementById('retry-btn')
767
+ newRetryBtn.addEventListener('click', () => this.loadScreenshots())
768
+ }
769
+
770
+ displayScreenshots(files) {
771
+ this.loadingEl.style.display = 'none'
772
+ this.errorEl.style.display = 'none'
773
+ this.containerEl.style.display = 'block'
774
+
775
+ // Update image count
776
+ const validImages = files.filter(file => this.isImageFile(file))
777
+ this.imageCountEl.textContent = `${validImages.length} image${validImages.length !== 1 ? 's' : ''}`
778
+
779
+ // Clear previous content
780
+ this.gridEl.innerHTML = ''
781
+
782
+ if (validImages.length === 0) {
783
+ this.gridEl.innerHTML = `
784
+ <div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: rgba(0,0,0,0.5);">
785
+ <i class="fa-solid fa-camera" style="font-size: 48px; margin-bottom: 16px; opacity: 0.3;"></i>
786
+ <p>No screenshots found</p>
787
+ <p style="font-size: 14px;">Take your first screenshot using the camera button in the header</p>
788
+ </div>
789
+ `
790
+ return
791
+ }
792
+
793
+ // Sort files by timestamp (newest first)
794
+ const sortedFiles = validImages.sort((a, b) => {
795
+ const timestampA = this.extractTimestamp(a)
796
+ const timestampB = this.extractTimestamp(b)
797
+ return timestampB - timestampA
798
+ })
799
+
800
+ // Create gallery items
801
+ sortedFiles.forEach(file => {
802
+ const item = this.createGalleryItem(file)
803
+ this.gridEl.appendChild(item)
804
+ })
805
+ }
806
+
807
+ isImageFile(filename) {
808
+ const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']
809
+ return imageExtensions.some(ext => filename.toLowerCase().endsWith(ext))
810
+ }
811
+
812
+ extractTimestamp(filename) {
813
+ // Extract timestamp from filename like "/asset/screenshots/1756972601674.png"
814
+ const match = filename.match(/(\d{13})/)
815
+ if (match) {
816
+ return parseInt(match[1])
817
+ }
818
+ return 0
819
+ }
820
+
821
+ extractFilename(filepath) {
822
+ return filepath.split('/').pop()
823
+ }
824
+
825
+ createGalleryItem(filepath) {
826
+ const container = document.createElement('div')
827
+ container.className = 'gallery-item'
828
+
829
+ const timestamp = this.extractTimestamp(filepath)
830
+ const filename = this.extractFilename(filepath)
831
+ const timeAgo = timestamp ? timeago.format(new Date(timestamp)) : 'Unknown time'
832
+
833
+ container.innerHTML = `
834
+ <img src="${filepath}" alt="${filename}" class="loading" loading="lazy">
835
+ <div class="gallery-item-actions">
836
+ <button class="gallery-item-action-btn gallery-item-delete-btn" title="Delete screenshot">
837
+ <i class="fa-solid fa-trash"></i>
838
+ </button>
839
+ </div>
840
+ <div class="gallery-item-info">
841
+ <div class="gallery-item-filename">${filename}</div>
842
+ <div class="gallery-item-timestamp">${timeAgo}</div>
843
+ </div>
844
+ `
845
+
846
+ const img = container.querySelector('img')
847
+
848
+ // Handle image loading
849
+ img.addEventListener('load', () => {
850
+ img.classList.remove('loading')
851
+ })
852
+
853
+ img.addEventListener('error', () => {
854
+ img.classList.remove('loading')
855
+ img.alt = 'Failed to load image'
856
+ img.style.background = 'rgba(0,0,0,0.1)'
857
+ img.style.display = 'flex'
858
+ img.style.alignItems = 'center'
859
+ img.style.justifyContent = 'center'
860
+ img.style.color = 'rgba(0,0,0,0.5)'
861
+ img.innerHTML = '<i class="fa-solid fa-image-slash"></i>'
862
+ })
863
+
864
+ // Handle delete button click
865
+ const deleteBtn = container.querySelector('.gallery-item-delete-btn')
866
+ deleteBtn.addEventListener('click', (e) => {
867
+ e.stopPropagation() // Prevent opening modal
868
+ this.confirmDelete(filepath, filename, container)
869
+ })
870
+
871
+ // Handle click to open in modal
872
+ container.addEventListener('click', (e) => {
873
+ // Don't open modal if clicking on action buttons
874
+ if (e.target.closest('.gallery-item-actions')) {
875
+ return
876
+ }
877
+ this.openModal(filepath, filename)
878
+ })
879
+
880
+ return container
881
+ }
882
+
883
+ openModal(imageSrc, filename) {
884
+ const modal = document.createElement('div')
885
+ modal.className = 'image-modal'
886
+ modal.innerHTML = `
887
+ <button class="image-modal-close" title="Close (Esc)">×</button>
888
+ <img src="${imageSrc}" alt="${filename}">
889
+ `
890
+
891
+ document.body.appendChild(modal)
892
+
893
+ // Close modal handlers
894
+ const closeBtn = modal.querySelector('.image-modal-close')
895
+ closeBtn.addEventListener('click', (e) => {
896
+ e.stopPropagation()
897
+ this.closeModal()
898
+ })
899
+
900
+ modal.addEventListener('click', () => {
901
+ this.closeModal()
902
+ })
903
+
904
+ // Prevent closing when clicking on image
905
+ const img = modal.querySelector('img')
906
+ img.addEventListener('click', (e) => {
907
+ e.stopPropagation()
908
+ })
909
+
910
+ // Store reference for closing
911
+ this.currentModal = modal
912
+ }
913
+
914
+ closeModal() {
915
+ if (this.currentModal) {
916
+ document.body.removeChild(this.currentModal)
917
+ this.currentModal = null
918
+ }
919
+ }
920
+
921
+ confirmDelete(filepath, filename, containerElement) {
922
+ Swal.fire({
923
+ title: 'Delete Screenshot?',
924
+ html: `
925
+ <div style="text-align: center; margin: 20px 0;">
926
+ <img src="${filepath}" style="max-width: 200px; max-height: 150px; border-radius: 4px; margin-bottom: 10px;" alt="Preview">
927
+ <p style="margin: 10px 0; font-weight: bold;">${filename}</p>
928
+ <p style="margin: 0; color: #666; font-size: 14px;">This action cannot be undone.</p>
929
+ </div>
930
+ `,
931
+ icon: 'warning',
932
+ showCancelButton: true,
933
+ confirmButtonColor: '#dc3545',
934
+ cancelButtonColor: '#6c757d',
935
+ confirmButtonText: 'Delete',
936
+ cancelButtonText: 'Cancel'
937
+ }).then((result) => {
938
+ if (result.isConfirmed) {
939
+ this.deleteScreenshot(filepath, filename, containerElement)
940
+ }
941
+ })
942
+ }
943
+
944
+ async deleteScreenshot(filepath, filename, containerElement) {
945
+ try {
946
+ // Add deleting state
947
+ containerElement.classList.add('deleting')
948
+
949
+ const response = await fetch('/snapshots', {
950
+ method: 'POST',
951
+ headers: {
952
+ 'Content-Type': 'application/json'
953
+ },
954
+ body: JSON.stringify({ filename: filepath })
955
+ })
956
+
957
+ const result = await response.json()
958
+
959
+ if (!response.ok) {
960
+ throw new Error(result.error || `HTTP ${response.status}`)
961
+ }
962
+
963
+ // Remove from DOM with animation
964
+ containerElement.style.transition = 'all 0.3s ease'
965
+ containerElement.style.transform = 'scale(0.8)'
966
+ containerElement.style.opacity = '0'
967
+
968
+ setTimeout(() => {
969
+ if (containerElement.parentNode) {
970
+ containerElement.parentNode.removeChild(containerElement)
971
+ }
972
+
973
+ // Update image count
974
+ const remainingImages = this.gridEl.children.length
975
+ this.imageCountEl.textContent = `${remainingImages} image${remainingImages !== 1 ? 's' : ''}`
976
+
977
+ // Show empty state if no images left
978
+ if (remainingImages === 0) {
979
+ this.gridEl.innerHTML = `
980
+ <div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: rgba(0,0,0,0.5);">
981
+ <i class="fa-solid fa-camera" style="font-size: 48px; margin-bottom: 16px; opacity: 0.3;"></i>
982
+ <p>No screenshots found</p>
983
+ <p style="font-size: 14px;">Take your first screenshot using the camera button in the header</p>
984
+ </div>
985
+ `
986
+ }
987
+ }, 300)
988
+
989
+ // Show success message
990
+ Swal.fire({
991
+ title: 'Deleted!',
992
+ text: 'Screenshot has been deleted.',
993
+ icon: 'success',
994
+ timer: 2000,
995
+ showConfirmButton: false
996
+ })
997
+
998
+ } catch (error) {
999
+ console.error('Failed to delete screenshot:', error)
1000
+
1001
+ // Remove deleting state
1002
+ containerElement.classList.remove('deleting')
1003
+
1004
+ // Show error message
1005
+ Swal.fire({
1006
+ title: 'Delete Failed',
1007
+ text: error.message,
1008
+ icon: 'error'
1009
+ })
1010
+ }
1011
+ }
1012
+ }
1013
+
1014
+ // Initialize gallery when page loads
1015
+ document.addEventListener('DOMContentLoaded', () => {
1016
+ new ScreenshotGallery()
1017
+ })
1018
+ </script>
1019
+ </body>
1020
+ </html>