@seedvault/server 0.1.4 → 0.2.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/dist/index.html +617 -59
- package/dist/server.js +445 -423
- package/package.json +1 -1
package/dist/index.html
CHANGED
|
@@ -74,6 +74,33 @@
|
|
|
74
74
|
cursor: pointer;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
#menu-toggle {
|
|
78
|
+
display: none;
|
|
79
|
+
font-size: 18px;
|
|
80
|
+
padding: 4px 10px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#nav-close {
|
|
84
|
+
display: none;
|
|
85
|
+
float: right;
|
|
86
|
+
border: none;
|
|
87
|
+
font-size: 16px;
|
|
88
|
+
padding: 0 4px;
|
|
89
|
+
line-height: 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#backdrop {
|
|
93
|
+
display: none;
|
|
94
|
+
position: fixed;
|
|
95
|
+
inset: 0;
|
|
96
|
+
background: rgba(0, 0, 0, 0.4);
|
|
97
|
+
z-index: 90;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#backdrop.visible {
|
|
101
|
+
display: block;
|
|
102
|
+
}
|
|
103
|
+
|
|
77
104
|
.grid {
|
|
78
105
|
display: flex;
|
|
79
106
|
gap: 0;
|
|
@@ -103,8 +130,32 @@
|
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
@media (max-width: 600px) {
|
|
133
|
+
#menu-toggle {
|
|
134
|
+
display: block;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#nav-close {
|
|
138
|
+
display: block;
|
|
139
|
+
}
|
|
140
|
+
|
|
106
141
|
.grid {
|
|
107
142
|
flex-direction: column;
|
|
143
|
+
position: relative;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#nav {
|
|
147
|
+
position: fixed;
|
|
148
|
+
top: 0;
|
|
149
|
+
left: -100vw;
|
|
150
|
+
width: 100vw !important;
|
|
151
|
+
height: 100%;
|
|
152
|
+
z-index: 100;
|
|
153
|
+
background: Canvas;
|
|
154
|
+
transition: transform 0.2s ease;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#nav.open {
|
|
158
|
+
transform: translateX(100vw);
|
|
108
159
|
}
|
|
109
160
|
|
|
110
161
|
.grid>.panel:first-child {
|
|
@@ -178,17 +229,31 @@
|
|
|
178
229
|
}
|
|
179
230
|
|
|
180
231
|
.tree-row {
|
|
181
|
-
display:
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: baseline;
|
|
182
234
|
width: 100%;
|
|
183
235
|
text-align: left;
|
|
184
236
|
border: none;
|
|
185
237
|
padding: 4px 8px;
|
|
186
238
|
white-space: nowrap;
|
|
187
239
|
overflow: hidden;
|
|
188
|
-
text-overflow: ellipsis;
|
|
189
240
|
cursor: pointer;
|
|
190
241
|
}
|
|
191
242
|
|
|
243
|
+
.tree-row .name {
|
|
244
|
+
overflow: hidden;
|
|
245
|
+
text-overflow: ellipsis;
|
|
246
|
+
flex: 1;
|
|
247
|
+
min-width: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.tree-row .ctime {
|
|
251
|
+
flex-shrink: 0;
|
|
252
|
+
margin-left: 8px;
|
|
253
|
+
opacity: 0.45;
|
|
254
|
+
font-size: 10px;
|
|
255
|
+
}
|
|
256
|
+
|
|
192
257
|
.tree-row:hover {
|
|
193
258
|
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
194
259
|
}
|
|
@@ -329,18 +394,256 @@
|
|
|
329
394
|
font-size: 11px;
|
|
330
395
|
opacity: 0.6;
|
|
331
396
|
flex-shrink: 0;
|
|
397
|
+
display: flex;
|
|
398
|
+
align-items: center;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
#status-text {
|
|
402
|
+
flex: 1;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#activity-btn {
|
|
406
|
+
border: none;
|
|
407
|
+
font-size: 11px;
|
|
408
|
+
padding: 0 4px;
|
|
409
|
+
opacity: 0.6;
|
|
410
|
+
display: flex;
|
|
411
|
+
align-items: center;
|
|
412
|
+
gap: 3px;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#activity-btn:hover {
|
|
416
|
+
opacity: 1;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#activity-modal {
|
|
420
|
+
display: none;
|
|
421
|
+
position: fixed;
|
|
422
|
+
inset: 0;
|
|
423
|
+
z-index: 300;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#activity-modal.open {
|
|
427
|
+
display: flex;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#activity-overlay {
|
|
431
|
+
position: absolute;
|
|
432
|
+
inset: 0;
|
|
433
|
+
background: rgba(0, 0, 0, 0.4);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
#activity-dialog {
|
|
437
|
+
position: absolute;
|
|
438
|
+
inset: 0;
|
|
439
|
+
background: Canvas;
|
|
440
|
+
color: CanvasText;
|
|
441
|
+
display: flex;
|
|
442
|
+
flex-direction: column;
|
|
443
|
+
overflow: hidden;
|
|
444
|
+
z-index: 1;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
#activity-dialog-head {
|
|
448
|
+
padding: 8px 12px;
|
|
449
|
+
border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
450
|
+
font-weight: bold;
|
|
451
|
+
font-size: 11px;
|
|
452
|
+
text-transform: uppercase;
|
|
453
|
+
letter-spacing: 0.05em;
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
flex-shrink: 0;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#activity-dialog-head button {
|
|
460
|
+
margin-left: auto;
|
|
461
|
+
border: none;
|
|
462
|
+
font-size: 16px;
|
|
463
|
+
padding: 0 4px;
|
|
464
|
+
line-height: 1;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#activity-dialog-body {
|
|
468
|
+
flex: 1;
|
|
469
|
+
overflow-y: auto;
|
|
470
|
+
scrollbar-width: thin;
|
|
471
|
+
scrollbar-color: color-mix(in srgb, currentColor 20%, transparent) transparent;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.activity-list {
|
|
475
|
+
padding: 16px 0;
|
|
476
|
+
margin: 0;
|
|
477
|
+
list-style: none;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.activity-item {
|
|
481
|
+
display: grid;
|
|
482
|
+
grid-template-columns: 90px 20px 1fr;
|
|
483
|
+
gap: 0 8px;
|
|
484
|
+
min-height: 40px;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.activity-time-col {
|
|
488
|
+
text-align: right;
|
|
489
|
+
padding-top: 6px;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.activity-time {
|
|
493
|
+
font-size: 11px;
|
|
494
|
+
opacity: 0.7;
|
|
495
|
+
white-space: nowrap;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.activity-date {
|
|
499
|
+
font-size: 10px;
|
|
500
|
+
opacity: 0.35;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.activity-dot-col {
|
|
504
|
+
position: relative;
|
|
505
|
+
display: flex;
|
|
506
|
+
flex-direction: column;
|
|
507
|
+
align-items: center;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.activity-dot-col::after {
|
|
511
|
+
content: '';
|
|
512
|
+
position: absolute;
|
|
513
|
+
left: 50%;
|
|
514
|
+
top: 0;
|
|
515
|
+
bottom: 0;
|
|
516
|
+
width: 1px;
|
|
517
|
+
transform: translateX(-50%);
|
|
518
|
+
background: color-mix(in srgb, currentColor 15%, transparent);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.activity-item:first-child .activity-dot-col::after {
|
|
522
|
+
top: 13px;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.activity-item:last-child .activity-dot-col::after {
|
|
526
|
+
bottom: calc(100% - 14px);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.activity-dot {
|
|
530
|
+
width: 5px;
|
|
531
|
+
height: 5px;
|
|
532
|
+
border-radius: 50%;
|
|
533
|
+
background: CanvasText;
|
|
534
|
+
opacity: 0.35;
|
|
535
|
+
flex-shrink: 0;
|
|
536
|
+
margin-top: 11px;
|
|
537
|
+
position: relative;
|
|
538
|
+
z-index: 1;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.activity-line {
|
|
542
|
+
display: none;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.activity-content {
|
|
546
|
+
padding: 6px 0 0;
|
|
547
|
+
min-width: 0;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.activity-action {
|
|
551
|
+
font-family: ui-monospace, monospace;
|
|
552
|
+
font-size: 11px;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.activity-by {
|
|
556
|
+
font-size: 11px;
|
|
557
|
+
opacity: 0.45;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.activity-contributor {
|
|
561
|
+
font-weight: 600;
|
|
562
|
+
opacity: 1;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.activity-detail {
|
|
566
|
+
margin-top: 2px;
|
|
567
|
+
font-size: 11px;
|
|
568
|
+
opacity: 0.45;
|
|
569
|
+
font-family: ui-monospace, monospace;
|
|
570
|
+
word-break: break-all;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.activity-diff {
|
|
574
|
+
margin-top: 4px;
|
|
575
|
+
font-family: ui-monospace, monospace;
|
|
576
|
+
font-size: 11px;
|
|
577
|
+
line-height: 1.4;
|
|
578
|
+
white-space: pre-wrap;
|
|
579
|
+
word-break: break-all;
|
|
580
|
+
padding: 4px 6px;
|
|
581
|
+
background: color-mix(in srgb, currentColor 4%, transparent);
|
|
582
|
+
border-radius: 3px;
|
|
583
|
+
overflow-x: auto;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.activity-diff .diff-add {
|
|
587
|
+
color: #22863a;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.activity-diff .diff-del {
|
|
591
|
+
color: #cb2431;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.activity-diff .diff-hunk {
|
|
595
|
+
color: #6f42c1;
|
|
596
|
+
opacity: 0.7;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
@media (prefers-color-scheme: dark) {
|
|
600
|
+
.activity-diff .diff-add {
|
|
601
|
+
color: #85e89d;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.activity-diff .diff-del {
|
|
605
|
+
color: #f97583;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.activity-diff .diff-hunk {
|
|
609
|
+
color: #b392f0;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.activity-diff-truncated {
|
|
614
|
+
font-size: 10px;
|
|
615
|
+
opacity: 0.5;
|
|
616
|
+
font-style: italic;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
#activity-load-more {
|
|
620
|
+
width: 100%;
|
|
621
|
+
padding: 8px;
|
|
622
|
+
border: none;
|
|
623
|
+
border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent);
|
|
624
|
+
font-size: 11px;
|
|
625
|
+
text-transform: uppercase;
|
|
626
|
+
letter-spacing: 0.05em;
|
|
627
|
+
opacity: 0.6;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#activity-load-more:hover {
|
|
631
|
+
opacity: 1;
|
|
632
|
+
background: color-mix(in srgb, currentColor 5%, transparent);
|
|
332
633
|
}
|
|
333
634
|
</style>
|
|
334
635
|
</head>
|
|
335
636
|
|
|
336
637
|
<body>
|
|
337
638
|
<div class="top">
|
|
639
|
+
<button id="menu-toggle" aria-label="Toggle navigation">☰</button>
|
|
338
640
|
<input id="token" type="password" placeholder="API key ("sv_...")" />
|
|
339
641
|
<button id="connect">Load</button>
|
|
340
642
|
</div>
|
|
643
|
+
<div id="backdrop"></div>
|
|
341
644
|
<div class="grid">
|
|
342
645
|
<section id="nav" class="panel" style="width:220px">
|
|
343
|
-
<div class="panel-head">Files
|
|
646
|
+
<div class="panel-head">Files<button id="nav-close" aria-label="Close navigation">×</button></div>
|
|
344
647
|
<div class="panel-body" id="nav-body" tabindex="0">
|
|
345
648
|
<ul id="files" class="tree"></ul>
|
|
346
649
|
</div>
|
|
@@ -353,11 +656,22 @@
|
|
|
353
656
|
</div>
|
|
354
657
|
</section>
|
|
355
658
|
</div>
|
|
356
|
-
<div id="
|
|
659
|
+
<div id="activity-modal">
|
|
660
|
+
<div id="activity-overlay"></div>
|
|
661
|
+
<div id="activity-dialog">
|
|
662
|
+
<div id="activity-dialog-head">Activity Log<button id="activity-close" aria-label="Close">×</button></div>
|
|
663
|
+
<div id="activity-dialog-body"></div>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
<div id="status">
|
|
667
|
+
<span id="status-text"></span>
|
|
668
|
+
<button id="activity-btn" aria-label="Activity log"><i class="ph ph-pulse"></i> Activity Log</button>
|
|
669
|
+
</div>
|
|
357
670
|
<script type="module">
|
|
358
671
|
const { marked } = await import("https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js");
|
|
359
672
|
const matter = (await import("https://cdn.jsdelivr.net/npm/gray-matter@4.0.3/+esm")).default;
|
|
360
673
|
const DOMPurify = (await import("https://cdn.jsdelivr.net/npm/dompurify@3.0.9/+esm")).default;
|
|
674
|
+
const morphdom = (await import("https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm")).default;
|
|
361
675
|
|
|
362
676
|
function renderMarkdown(raw) {
|
|
363
677
|
if (typeof raw !== "string") return "";
|
|
@@ -379,12 +693,28 @@
|
|
|
379
693
|
const tokenEl = $("token");
|
|
380
694
|
const filesEl = $("files");
|
|
381
695
|
const contentEl = $("content");
|
|
382
|
-
const
|
|
696
|
+
const statusTextEl = $("status-text");
|
|
697
|
+
const backdropEl = $("backdrop");
|
|
698
|
+
const menuToggleEl = $("menu-toggle");
|
|
699
|
+
|
|
700
|
+
function closeSidebar() {
|
|
701
|
+
$("nav").classList.remove("open");
|
|
702
|
+
backdropEl.classList.remove("visible");
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
menuToggleEl.addEventListener("click", () => {
|
|
706
|
+
const nav = $("nav");
|
|
707
|
+
nav.classList.toggle("open");
|
|
708
|
+
backdropEl.classList.toggle("visible", nav.classList.contains("open"));
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
backdropEl.addEventListener("click", closeSidebar);
|
|
712
|
+
$("nav-close").addEventListener("click", closeSidebar);
|
|
383
713
|
|
|
384
714
|
let token = localStorage.getItem("sv-token") || "";
|
|
385
715
|
tokenEl.value = token;
|
|
386
716
|
|
|
387
|
-
function status(msg) {
|
|
717
|
+
function status(msg) { statusTextEl.textContent = msg; }
|
|
388
718
|
|
|
389
719
|
async function api(url, opts = {}) {
|
|
390
720
|
const res = await fetch(url, {
|
|
@@ -404,6 +734,15 @@
|
|
|
404
734
|
const savedContributor = localStorage.getItem("sv-contributor") || "";
|
|
405
735
|
const savedFile = localStorage.getItem("sv-file") || "";
|
|
406
736
|
|
|
737
|
+
function getExpandedKeys() {
|
|
738
|
+
try {
|
|
739
|
+
return new Set(JSON.parse(localStorage.getItem("sv-expanded") || "[]"));
|
|
740
|
+
} catch { return new Set(); }
|
|
741
|
+
}
|
|
742
|
+
function saveExpandedKeys(keys) {
|
|
743
|
+
localStorage.setItem("sv-expanded", JSON.stringify([...keys]));
|
|
744
|
+
}
|
|
745
|
+
|
|
407
746
|
function buildTree(fileEntries) {
|
|
408
747
|
const root = {};
|
|
409
748
|
for (const f of fileEntries) {
|
|
@@ -418,8 +757,18 @@
|
|
|
418
757
|
return root;
|
|
419
758
|
}
|
|
420
759
|
|
|
760
|
+
function formatCtime(iso) {
|
|
761
|
+
if (!iso) return "";
|
|
762
|
+
const d = new Date(iso);
|
|
763
|
+
const mon = d.toLocaleString(undefined, { month: "short" });
|
|
764
|
+
const day = d.getDate();
|
|
765
|
+
const now = new Date();
|
|
766
|
+
if (d.getFullYear() === now.getFullYear()) return mon + " " + day;
|
|
767
|
+
return mon + " " + day + ", " + d.getFullYear();
|
|
768
|
+
}
|
|
769
|
+
|
|
421
770
|
function getNewestCtime(node) {
|
|
422
|
-
if (node.__file) return node.__file.
|
|
771
|
+
if (node.__file) return node.__file.createdAt || "";
|
|
423
772
|
let newest = "";
|
|
424
773
|
for (const key of Object.keys(node)) {
|
|
425
774
|
if (key === "__file") continue;
|
|
@@ -438,7 +787,7 @@
|
|
|
438
787
|
return count;
|
|
439
788
|
}
|
|
440
789
|
|
|
441
|
-
function renderTree(node, parentUl, username, depth) {
|
|
790
|
+
function renderTree(node, parentUl, username, depth, pathPrefix = "") {
|
|
442
791
|
const keys = Object.keys(node).filter((k) => k !== "__file").sort((a, b) => {
|
|
443
792
|
const aDir = Object.keys(node[a]).some((k) => k !== "__file");
|
|
444
793
|
const bDir = Object.keys(node[b]).some((k) => k !== "__file");
|
|
@@ -450,26 +799,30 @@
|
|
|
450
799
|
});
|
|
451
800
|
for (const key of keys) {
|
|
452
801
|
const child = node[key];
|
|
802
|
+
const nodePath = pathPrefix ? pathPrefix + "/" + key : key;
|
|
453
803
|
const isFile = child.__file && Object.keys(child).length === 1;
|
|
454
804
|
const li = document.createElement("li");
|
|
805
|
+
li.dataset.key = nodePath;
|
|
455
806
|
const row = document.createElement("div");
|
|
456
807
|
row.className = "tree-row";
|
|
457
808
|
row.style.paddingLeft = (depth * 12 + 8) + "px";
|
|
458
809
|
|
|
459
810
|
if (isFile) {
|
|
460
|
-
|
|
811
|
+
const ctime = child.__file.createdAt || "";
|
|
812
|
+
row.innerHTML = '<span class="name"><span class="arrow"> </span><i class="ph ph-file-text"></i> ' + key + '</span><span class="ctime">' + formatCtime(ctime) + '</span>';
|
|
461
813
|
row.dataset.path = child.__file.path;
|
|
462
814
|
row.dataset.contributor = username;
|
|
463
|
-
row.
|
|
815
|
+
row.dataset.action = "open";
|
|
464
816
|
} else {
|
|
465
817
|
const sub = document.createElement("ul");
|
|
466
818
|
const fileCount = countFiles(child);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
819
|
+
const expanded = getExpandedKeys().has(username + ":" + nodePath);
|
|
820
|
+
const arrow = expanded ? "▼" : "▶";
|
|
821
|
+
if (!expanded) sub.classList.add("collapsed");
|
|
822
|
+
row.innerHTML = '<span class="name"><span class="arrow">' + arrow + '</span><i class="ph ph-folder"></i> ' + key + ' <span style="opacity:0.5">(' + fileCount + ')</span></span>';
|
|
823
|
+
row.dataset.action = "toggle";
|
|
824
|
+
row.dataset.toggleKey = username + ":" + nodePath;
|
|
825
|
+
renderTree(child, sub, username, depth + 1, nodePath);
|
|
473
826
|
li.appendChild(row);
|
|
474
827
|
li.appendChild(sub);
|
|
475
828
|
parentUl.appendChild(li);
|
|
@@ -530,9 +883,46 @@
|
|
|
530
883
|
);
|
|
531
884
|
}
|
|
532
885
|
|
|
886
|
+
filesEl.addEventListener("click", (e) => {
|
|
887
|
+
const row = e.target.closest(".tree-row");
|
|
888
|
+
if (!row) return;
|
|
889
|
+
const action = row.dataset.action;
|
|
890
|
+
if (action === "open") {
|
|
891
|
+
loadContent(row.dataset.contributor, row.dataset.path, row);
|
|
892
|
+
} else if (action === "toggle") {
|
|
893
|
+
const sub = row.nextElementSibling;
|
|
894
|
+
if (!sub) return;
|
|
895
|
+
sub.classList.toggle("collapsed");
|
|
896
|
+
const arrow = row.querySelector(".arrow");
|
|
897
|
+
if (arrow) arrow.innerHTML = sub.classList.contains("collapsed") ? "▶" : "▼";
|
|
898
|
+
const keys = getExpandedKeys();
|
|
899
|
+
const toggleKey = row.dataset.toggleKey;
|
|
900
|
+
if (toggleKey) {
|
|
901
|
+
if (sub.classList.contains("collapsed")) keys.delete(toggleKey);
|
|
902
|
+
else keys.add(toggleKey);
|
|
903
|
+
saveExpandedKeys(keys);
|
|
904
|
+
}
|
|
905
|
+
} else if (action === "contributor") {
|
|
906
|
+
const sub = row.nextElementSibling;
|
|
907
|
+
if (!sub) return;
|
|
908
|
+
const collapsed = sub.classList.contains("collapsed");
|
|
909
|
+
const keys = getExpandedKeys();
|
|
910
|
+
const toggleKey = "contributor:" + row.dataset.contributor;
|
|
911
|
+
if (collapsed) {
|
|
912
|
+
sub.classList.remove("collapsed");
|
|
913
|
+
row.querySelector(".arrow").innerHTML = "▼";
|
|
914
|
+
keys.add(toggleKey);
|
|
915
|
+
} else {
|
|
916
|
+
sub.classList.add("collapsed");
|
|
917
|
+
row.querySelector(".arrow").innerHTML = "▶";
|
|
918
|
+
keys.delete(toggleKey);
|
|
919
|
+
}
|
|
920
|
+
saveExpandedKeys(keys);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
533
924
|
async function loadContributorFiles(username, sub, opts = {}) {
|
|
534
925
|
const silent = !!opts.silent;
|
|
535
|
-
sub.innerHTML = "";
|
|
536
926
|
if (!silent) status("Loading files...");
|
|
537
927
|
const { files } = await (await api("/v1/files?prefix=" + encodeURIComponent(username + "/"))).json();
|
|
538
928
|
const prefix = username + "/";
|
|
@@ -540,14 +930,24 @@
|
|
|
540
930
|
...f,
|
|
541
931
|
path: f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path,
|
|
542
932
|
})));
|
|
543
|
-
|
|
933
|
+
const tmp = document.createElement("ul");
|
|
934
|
+
renderTree(tree, tmp, username, 1);
|
|
935
|
+
if (sub.hasChildNodes()) {
|
|
936
|
+
morphdom(sub, tmp, {
|
|
937
|
+
childrenOnly: true,
|
|
938
|
+
getNodeKey(node) {
|
|
939
|
+
return node.dataset?.key || "";
|
|
940
|
+
},
|
|
941
|
+
});
|
|
942
|
+
} else {
|
|
943
|
+
sub.append(...tmp.childNodes);
|
|
944
|
+
}
|
|
544
945
|
markActiveRow();
|
|
545
946
|
if (!silent) status(files.length + " file(s)");
|
|
546
|
-
// Update contributor row with file count
|
|
547
947
|
const row = sub.parentElement && sub.parentElement.querySelector(".tree-row");
|
|
548
948
|
if (row) {
|
|
549
949
|
const arrow = row.querySelector(".arrow").outerHTML;
|
|
550
|
-
row.innerHTML = arrow + '<i class="ph ph-user"></i> ' + username + ' <span style="opacity:0.5">(' + files.length + ')</span>';
|
|
950
|
+
row.innerHTML = '<span class="name">' + arrow + '<i class="ph ph-user"></i> ' + username + ' <span style="opacity:0.5">(' + files.length + ')</span></span>';
|
|
551
951
|
}
|
|
552
952
|
}
|
|
553
953
|
|
|
@@ -556,6 +956,7 @@
|
|
|
556
956
|
contentEl.innerHTML = "";
|
|
557
957
|
status("Loading contributors...");
|
|
558
958
|
const { contributors } = await (await api("/v1/contributors")).json();
|
|
959
|
+
const expandedKeys = getExpandedKeys();
|
|
559
960
|
|
|
560
961
|
for (const b of contributors) {
|
|
561
962
|
const li = document.createElement("li");
|
|
@@ -564,40 +965,28 @@
|
|
|
564
965
|
row.style.paddingLeft = "8px";
|
|
565
966
|
const sub = document.createElement("ul");
|
|
566
967
|
sub.dataset.contributor = b.username;
|
|
567
|
-
sub.dataset.loaded = "
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
row.querySelector(".arrow").innerHTML = "▼";
|
|
575
|
-
if (!loaded) {
|
|
576
|
-
loaded = true;
|
|
577
|
-
sub.dataset.loaded = "true";
|
|
578
|
-
await loadContributorFiles(b.username, sub);
|
|
579
|
-
}
|
|
580
|
-
} else {
|
|
581
|
-
sub.classList.add("collapsed");
|
|
582
|
-
row.querySelector(".arrow").innerHTML = "▶";
|
|
583
|
-
}
|
|
584
|
-
};
|
|
968
|
+
sub.dataset.loaded = "true";
|
|
969
|
+
const expanded = expandedKeys.has("contributor:" + b.username);
|
|
970
|
+
if (!expanded) sub.classList.add("collapsed");
|
|
971
|
+
const arrow = expanded ? "▼" : "▶";
|
|
972
|
+
row.innerHTML = '<span class="name"><span class="arrow">' + arrow + '</span><i class="ph ph-user"></i> ' + b.username + '</span>';
|
|
973
|
+
row.dataset.action = "contributor";
|
|
974
|
+
row.dataset.contributor = b.username;
|
|
585
975
|
li.appendChild(row);
|
|
586
976
|
li.appendChild(sub);
|
|
587
977
|
filesEl.appendChild(li);
|
|
978
|
+
}
|
|
588
979
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (match) await loadContent(b.username, savedFile, match);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
980
|
+
await Promise.all(contributors.map((b) => {
|
|
981
|
+
const sub = getLoadedContributorList(b.username);
|
|
982
|
+
return sub ? loadContributorFiles(b.username, sub, { silent: true }) : null;
|
|
983
|
+
}));
|
|
984
|
+
|
|
985
|
+
if (savedFile && savedContributor) {
|
|
986
|
+
const match = filesEl.querySelector('[data-path="' + CSS.escape(savedFile) + '"]');
|
|
987
|
+
if (match) await loadContent(savedContributor, savedFile, match);
|
|
600
988
|
}
|
|
989
|
+
|
|
601
990
|
status(contributors.length + " contributor(s)");
|
|
602
991
|
}
|
|
603
992
|
|
|
@@ -607,12 +996,10 @@
|
|
|
607
996
|
row.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
608
997
|
localStorage.setItem("sv-contributor", username);
|
|
609
998
|
localStorage.setItem("sv-file", path);
|
|
999
|
+
closeSidebar();
|
|
610
1000
|
status("Loading " + path + "...");
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
headers: { "Content-Type": "application/json" },
|
|
614
|
-
body: JSON.stringify({ cmd: 'cat "' + username + "/" + path + '"' }),
|
|
615
|
-
});
|
|
1001
|
+
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
|
1002
|
+
const res = await api("/v1/files/" + encodeURIComponent(username) + "/" + encodedPath);
|
|
616
1003
|
const text = await res.text();
|
|
617
1004
|
contentEl.innerHTML = renderMarkdown(text);
|
|
618
1005
|
contentEl.parentElement.scrollTop = 0;
|
|
@@ -656,11 +1043,8 @@
|
|
|
656
1043
|
scheduleContributorReload(contributor);
|
|
657
1044
|
// If this file is currently open, reload its content
|
|
658
1045
|
if (localStorage.getItem("sv-file") === path && localStorage.getItem("sv-contributor") === contributor) {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
headers: { "Content-Type": "application/json" },
|
|
662
|
-
body: JSON.stringify({ cmd: 'cat "' + contributor + "/" + path + '"' }),
|
|
663
|
-
})
|
|
1046
|
+
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
|
1047
|
+
api("/v1/files/" + encodeURIComponent(contributor) + "/" + encodedPath)
|
|
664
1048
|
.then((res) => res.text())
|
|
665
1049
|
.then((text) => { contentEl.innerHTML = renderMarkdown(text); });
|
|
666
1050
|
}
|
|
@@ -676,6 +1060,10 @@
|
|
|
676
1060
|
}
|
|
677
1061
|
});
|
|
678
1062
|
|
|
1063
|
+
evtSource.addEventListener("activity", (e) => {
|
|
1064
|
+
handleActivitySSE(e);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
679
1067
|
evtSource.onerror = () => {
|
|
680
1068
|
// EventSource auto-reconnects
|
|
681
1069
|
};
|
|
@@ -717,6 +1105,176 @@
|
|
|
717
1105
|
document.addEventListener("mousemove", onMove);
|
|
718
1106
|
document.addEventListener("mouseup", onUp);
|
|
719
1107
|
});
|
|
1108
|
+
|
|
1109
|
+
// --- Activity modal ---
|
|
1110
|
+
|
|
1111
|
+
const activityModal = $("activity-modal");
|
|
1112
|
+
const activityBody = $("activity-dialog-body");
|
|
1113
|
+
const ACTIVITY_PAGE_SIZE = 1000;
|
|
1114
|
+
let activitySeenIds = new Set();
|
|
1115
|
+
let activityOffset = 0;
|
|
1116
|
+
let activityHasMore = false;
|
|
1117
|
+
|
|
1118
|
+
function openActivityModal() {
|
|
1119
|
+
activitySeenIds = new Set();
|
|
1120
|
+
activityOffset = 0;
|
|
1121
|
+
activityBody.innerHTML = "";
|
|
1122
|
+
activityModal.classList.add("open");
|
|
1123
|
+
loadActivityPage();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function closeActivityModal() {
|
|
1127
|
+
activityModal.classList.remove("open");
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function handleActivitySSE(ev) {
|
|
1131
|
+
if (!activityModal.classList.contains("open")) return;
|
|
1132
|
+
const event = JSON.parse(ev.detail || ev.data);
|
|
1133
|
+
if (activitySeenIds.has(event.id)) return;
|
|
1134
|
+
activitySeenIds.add(event.id);
|
|
1135
|
+
activityOffset++;
|
|
1136
|
+
let list = activityBody.querySelector(".activity-list");
|
|
1137
|
+
if (!list) {
|
|
1138
|
+
const empty = activityBody.querySelector("p");
|
|
1139
|
+
if (empty) empty.remove();
|
|
1140
|
+
list = document.createElement("ul");
|
|
1141
|
+
list.className = "activity-list";
|
|
1142
|
+
activityBody.prepend(list);
|
|
1143
|
+
}
|
|
1144
|
+
list.insertAdjacentHTML("afterbegin", renderActivityItems([event]));
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
$("activity-btn").addEventListener("click", openActivityModal);
|
|
1148
|
+
$("activity-close").addEventListener("click", closeActivityModal);
|
|
1149
|
+
$("activity-overlay").addEventListener("click", closeActivityModal);
|
|
1150
|
+
|
|
1151
|
+
document.addEventListener("keydown", (e) => {
|
|
1152
|
+
if (e.key === "Escape" && activityModal.classList.contains("open")) {
|
|
1153
|
+
closeActivityModal();
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
function formatActivityTime(iso) {
|
|
1158
|
+
if (!iso) return { time: "", date: "" };
|
|
1159
|
+
const d = new Date(iso);
|
|
1160
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
1161
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
1162
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
1163
|
+
const ms = String(d.getMilliseconds()).padStart(3, "0");
|
|
1164
|
+
const mon = d.toLocaleString(undefined, { month: "short" });
|
|
1165
|
+
const day = d.getDate();
|
|
1166
|
+
const yr = d.getFullYear();
|
|
1167
|
+
const now = new Date();
|
|
1168
|
+
const dateStr = yr === now.getFullYear()
|
|
1169
|
+
? mon + " " + day
|
|
1170
|
+
: mon + " " + day + ", " + yr;
|
|
1171
|
+
return {
|
|
1172
|
+
time: hh + ":" + mm + ":" + ss + "." + ms,
|
|
1173
|
+
date: dateStr,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function parseDetail(raw) {
|
|
1178
|
+
if (!raw) return null;
|
|
1179
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function esc(str) {
|
|
1183
|
+
return String(str)
|
|
1184
|
+
.replace(/&/g, "&")
|
|
1185
|
+
.replace(/</g, "<")
|
|
1186
|
+
.replace(/>/g, ">")
|
|
1187
|
+
.replace(/"/g, """);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function formatDetail(detail) {
|
|
1191
|
+
if (!detail || typeof detail !== "object") return "";
|
|
1192
|
+
return Object.entries(detail)
|
|
1193
|
+
.filter(([k]) => k !== "diff" && k !== "diff_truncated")
|
|
1194
|
+
.map(([k, v]) => k + "=" + v)
|
|
1195
|
+
.join(" ");
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function renderDiff(raw) {
|
|
1199
|
+
if (!raw) return "";
|
|
1200
|
+
const lines = raw.split("\n");
|
|
1201
|
+
const htmlLines = lines.map((line) => {
|
|
1202
|
+
const escaped = esc(line);
|
|
1203
|
+
if (line.startsWith("@@")) return '<span class="diff-hunk">' + escaped + '</span>';
|
|
1204
|
+
if (line.startsWith("+")) return '<span class="diff-add">' + escaped + '</span>';
|
|
1205
|
+
if (line.startsWith("-")) return '<span class="diff-del">' + escaped + '</span>';
|
|
1206
|
+
return escaped;
|
|
1207
|
+
});
|
|
1208
|
+
return htmlLines.join("\n");
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function renderActivityItems(events) {
|
|
1212
|
+
return events.map((ev) => {
|
|
1213
|
+
const { time, date } = formatActivityTime(ev.created_at);
|
|
1214
|
+
const parsed = parseDetail(ev.detail);
|
|
1215
|
+
const detail = formatDetail(parsed);
|
|
1216
|
+
const diff = parsed?.diff || null;
|
|
1217
|
+
const truncated = parsed?.diff_truncated || false;
|
|
1218
|
+
|
|
1219
|
+
let content = '<span class="activity-action">' + esc(ev.action) + '</span>' +
|
|
1220
|
+
' <span class="activity-by">by</span>' +
|
|
1221
|
+
' <span class="activity-contributor">' + esc(ev.contributor) + '</span>';
|
|
1222
|
+
if (detail) content += '<div class="activity-detail">' + esc(detail) + '</div>';
|
|
1223
|
+
if (diff) {
|
|
1224
|
+
content += '<div class="activity-diff">' + renderDiff(diff) + '</div>';
|
|
1225
|
+
if (truncated) content += '<span class="activity-diff-truncated">diff truncated</span>';
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
return '<li class="activity-item">' +
|
|
1229
|
+
'<div class="activity-time-col">' +
|
|
1230
|
+
'<div class="activity-time">' + time + '</div>' +
|
|
1231
|
+
'<div class="activity-date">' + date + '</div>' +
|
|
1232
|
+
'</div>' +
|
|
1233
|
+
'<div class="activity-dot-col">' +
|
|
1234
|
+
'<div class="activity-dot"></div>' +
|
|
1235
|
+
'<div class="activity-line"></div>' +
|
|
1236
|
+
'</div>' +
|
|
1237
|
+
'<div class="activity-content">' + content + '</div>' +
|
|
1238
|
+
'</li>';
|
|
1239
|
+
}).join("");
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async function loadActivityPage() {
|
|
1243
|
+
const existingBtn = activityBody.querySelector("#activity-load-more");
|
|
1244
|
+
if (existingBtn) existingBtn.remove();
|
|
1245
|
+
|
|
1246
|
+
const limit = ACTIVITY_PAGE_SIZE;
|
|
1247
|
+
const offset = activityOffset;
|
|
1248
|
+
const url = "/v1/activity?limit=" + (limit + 1) + "&offset=" + offset;
|
|
1249
|
+
const { events } = await (await api(url)).json();
|
|
1250
|
+
|
|
1251
|
+
activityHasMore = events.length > limit;
|
|
1252
|
+
const page = activityHasMore ? events.slice(0, limit) : events;
|
|
1253
|
+
for (const ev of page) activitySeenIds.add(ev.id);
|
|
1254
|
+
activityOffset += page.length;
|
|
1255
|
+
|
|
1256
|
+
let list = activityBody.querySelector(".activity-list");
|
|
1257
|
+
if (!list) {
|
|
1258
|
+
list = document.createElement("ul");
|
|
1259
|
+
list.className = "activity-list";
|
|
1260
|
+
activityBody.appendChild(list);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (activitySeenIds.size === 0) {
|
|
1264
|
+
activityBody.innerHTML = '<p style="padding:12px;opacity:0.5;font-size:12px">No activity.</p>';
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
list.insertAdjacentHTML("beforeend", renderActivityItems(page));
|
|
1269
|
+
|
|
1270
|
+
if (activityHasMore) {
|
|
1271
|
+
const btn = document.createElement("button");
|
|
1272
|
+
btn.id = "activity-load-more";
|
|
1273
|
+
btn.textContent = "Load more";
|
|
1274
|
+
btn.addEventListener("click", () => loadActivityPage());
|
|
1275
|
+
activityBody.appendChild(btn);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
720
1278
|
</script>
|
|
721
1279
|
</body>
|
|
722
1280
|
|