@jidekaixin2dian/dsh-file-actions 0.1.11 → 0.1.13
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/CHANGELOG.md +71 -60
- package/README.en.md +124 -124
- package/README.md +127 -124
- package/SECURITY.md +30 -28
- package/lib/client.js +2300 -2237
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +73 -73
package/lib/client.js
CHANGED
|
@@ -1,2237 +1,2300 @@
|
|
|
1
|
-
// dsh-file-actions — file and link actions (web bundle, ModuleLoader CJS).
|
|
2
|
-
//
|
|
3
|
-
// Every file reference the Web GUI renders (inline-code links in the closing
|
|
4
|
-
// prose, delivery cards, changed-file rows, tool-call rows, the right Sidebar
|
|
5
|
-
// file tree, the review tab) is a plain DOM node carrying the Host path in a
|
|
6
|
-
// title/data attribute or as its visible text. This module listens for
|
|
7
|
-
// `contextmenu` in the capture phase, resolves that node to a real Host path
|
|
8
|
-
// through the shipped Remotes, and replaces Chromium's link menu with one that
|
|
9
|
-
// acts on the file.
|
|
10
|
-
//
|
|
11
|
-
// No host half is needed: opening and revealing go through
|
|
12
|
-
// `session.openWorkspacePath` (the same Session-authorized route the document
|
|
13
|
-
// preview uses), file associations through `session.workspacePathApplications`,
|
|
14
|
-
// and contents/save-as through `workspaceFiles.readBytes`. All three validate
|
|
15
|
-
// the path against the composed filesystem before touching the desktop.
|
|
16
|
-
// DSH concatenates classic bundles; every registration needs its own scope.
|
|
17
|
-
;(() => {
|
|
18
|
-
const factory = () => {
|
|
19
|
-
const module = { exports: {} }
|
|
20
|
-
const exports = module.exports
|
|
21
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
22
|
-
|
|
23
|
-
const MENU_ATTR = 'data-dfa-menu'
|
|
24
|
-
const ITEM_ATTR = 'data-dfa-item'
|
|
25
|
-
// One live install per page: a second materialization (HMR, a re-enabled row)
|
|
26
|
-
// would otherwise register a second contextmenu listener and open two menus.
|
|
27
|
-
let activeCleanup = null
|
|
28
|
-
|
|
29
|
-
/* ─────────────────────────────── copy ─────────────────────────────── */
|
|
30
|
-
|
|
31
|
-
const ZH = {
|
|
32
|
-
open: '打开文件',
|
|
33
|
-
openFolder: '打开文件夹',
|
|
34
|
-
openIn: (name) => `在 ${name} 中打开`,
|
|
35
|
-
openWith: '打开方式',
|
|
36
|
-
reveal: '在资源管理器中显示',
|
|
37
|
-
revealFolder: '在资源管理器中打开',
|
|
38
|
-
browser: '在浏览器中打开',
|
|
39
|
-
copyPath: '复制路径',
|
|
40
|
-
copyRelPath: '复制相对路径',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
'
|
|
220
|
-
|
|
221
|
-
'
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
sections.push(
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/* ───────────────────────────────
|
|
681
|
-
|
|
682
|
-
const
|
|
683
|
-
.
|
|
684
|
-
|
|
685
|
-
.
|
|
686
|
-
.
|
|
687
|
-
|
|
688
|
-
.
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
.dfa-
|
|
729
|
-
.
|
|
730
|
-
.dfa-
|
|
731
|
-
.dfa-
|
|
732
|
-
|
|
733
|
-
.dfa-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
color:var(--
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
.
|
|
749
|
-
.dfa-menu
|
|
750
|
-
|
|
751
|
-
.dfa-item
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
.dfa-
|
|
755
|
-
|
|
756
|
-
.dfa-
|
|
757
|
-
.dfa-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
.dfa-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
.dfa-
|
|
771
|
-
.dfa-
|
|
772
|
-
.dfa-
|
|
773
|
-
.dfa-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
.dfa-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
.dfa-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
function
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
if (
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
if (!
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1086
|
-
const
|
|
1087
|
-
if (
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
this.
|
|
1201
|
-
this.
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
this.
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
this
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
const
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
this.
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
this.
|
|
1265
|
-
this.
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
})
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
this.
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
this.
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
this.
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
this.
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
this.
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
if (
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
this.
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
if (
|
|
1619
|
-
event.preventDefault()
|
|
1620
|
-
this.
|
|
1621
|
-
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
return false
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
//
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
const
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
const ok = await copyText(
|
|
1857
|
-
toast(ok ? t.
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
const
|
|
2051
|
-
if (
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
}
|
|
2237
|
-
|
|
1
|
+
// dsh-file-actions — file and link actions (web bundle, ModuleLoader CJS).
|
|
2
|
+
//
|
|
3
|
+
// Every file reference the Web GUI renders (inline-code links in the closing
|
|
4
|
+
// prose, delivery cards, changed-file rows, tool-call rows, the right Sidebar
|
|
5
|
+
// file tree, the review tab) is a plain DOM node carrying the Host path in a
|
|
6
|
+
// title/data attribute or as its visible text. This module listens for
|
|
7
|
+
// `contextmenu` in the capture phase, resolves that node to a real Host path
|
|
8
|
+
// through the shipped Remotes, and replaces Chromium's link menu with one that
|
|
9
|
+
// acts on the file.
|
|
10
|
+
//
|
|
11
|
+
// No host half is needed: opening and revealing go through
|
|
12
|
+
// `session.openWorkspacePath` (the same Session-authorized route the document
|
|
13
|
+
// preview uses), file associations through `session.workspacePathApplications`,
|
|
14
|
+
// and contents/save-as through `workspaceFiles.readBytes`. All three validate
|
|
15
|
+
// the path against the composed filesystem before touching the desktop.
|
|
16
|
+
// DSH concatenates classic bundles; every registration needs its own scope.
|
|
17
|
+
;(() => {
|
|
18
|
+
const factory = () => {
|
|
19
|
+
const module = { exports: {} }
|
|
20
|
+
const exports = module.exports
|
|
21
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
22
|
+
|
|
23
|
+
const MENU_ATTR = 'data-dfa-menu'
|
|
24
|
+
const ITEM_ATTR = 'data-dfa-item'
|
|
25
|
+
// One live install per page: a second materialization (HMR, a re-enabled row)
|
|
26
|
+
// would otherwise register a second contextmenu listener and open two menus.
|
|
27
|
+
let activeCleanup = null
|
|
28
|
+
|
|
29
|
+
/* ─────────────────────────────── copy ─────────────────────────────── */
|
|
30
|
+
|
|
31
|
+
const ZH = {
|
|
32
|
+
open: '打开文件',
|
|
33
|
+
openFolder: '打开文件夹',
|
|
34
|
+
openIn: (name) => `在 ${name} 中打开`,
|
|
35
|
+
openWith: '打开方式',
|
|
36
|
+
reveal: '在资源管理器中显示',
|
|
37
|
+
revealFolder: '在资源管理器中打开',
|
|
38
|
+
browser: '在浏览器中打开',
|
|
39
|
+
copyPath: '复制路径',
|
|
40
|
+
copyRelPath: '复制相对路径',
|
|
41
|
+
copyFileName: '复制文件名',
|
|
42
|
+
copyReference: '复制引用',
|
|
43
|
+
copyMarkdown: '复制为 Markdown',
|
|
44
|
+
copyContents: '复制文件内容',
|
|
45
|
+
saveAs: '另存为…',
|
|
46
|
+
copiedPath: '路径已复制',
|
|
47
|
+
copiedRelPath: '相对路径已复制',
|
|
48
|
+
copiedFileName: '文件名已复制',
|
|
49
|
+
copiedReference: '引用已复制',
|
|
50
|
+
copiedMarkdown: 'Markdown 已复制',
|
|
51
|
+
lineAt: '第 {line} 行',
|
|
52
|
+
lineRange: '第 {from}–{to} 行',
|
|
53
|
+
copiedContents: '文件内容已复制',
|
|
54
|
+
copyFailed: '复制失败',
|
|
55
|
+
readFailed: '读取失败',
|
|
56
|
+
tooLarge: '文件过大,请改用「另存为」',
|
|
57
|
+
binary: '二进制文件,无法复制为文本',
|
|
58
|
+
confirmOutsideCopy: '该文件在会话工作区之外,不是本对话的一部分。确认要把它的内容复制到剪贴板吗?\n{path}',
|
|
59
|
+
openFailed: '打开失败',
|
|
60
|
+
revealFailed: '无法定位该文件',
|
|
61
|
+
popupBlocked: '浏览器拦截了新窗口',
|
|
62
|
+
noSession: '没有可用的会话',
|
|
63
|
+
loading: '正在读取…',
|
|
64
|
+
dir: '文件夹',
|
|
65
|
+
notFound: '找不到该文件',
|
|
66
|
+
outsideWorkspace: '工作区外',
|
|
67
|
+
defaultApp: '默认',
|
|
68
|
+
appsUnit: '个应用',
|
|
69
|
+
openLink: '打开链接',
|
|
70
|
+
copyLink: '复制链接',
|
|
71
|
+
copyLinkText: '复制链接文字',
|
|
72
|
+
copiedLink: '链接已复制',
|
|
73
|
+
copiedLinkText: '链接文字已复制',
|
|
74
|
+
keysMove: '↑↓ 选择',
|
|
75
|
+
keysRun: 'Enter 执行',
|
|
76
|
+
keysClose: 'Esc 关闭',
|
|
77
|
+
searchApps: '搜索应用…',
|
|
78
|
+
noMatch: '没有匹配的应用',
|
|
79
|
+
appsLoading: '正在查询关联应用…',
|
|
80
|
+
appsEmpty: '没有关联应用',
|
|
81
|
+
appsFailed: '应用查询失败,点击重试',
|
|
82
|
+
preferred: '首选',
|
|
83
|
+
pinApp: '设为此文件类型的首选应用',
|
|
84
|
+
unpinApp: '取消此文件类型的首选应用',
|
|
85
|
+
pinHint: 'Shift+Enter 设置或取消首选',
|
|
86
|
+
preferenceSaved: '首选应用已保存',
|
|
87
|
+
preferenceCleared: '已恢复自动选择应用',
|
|
88
|
+
preferenceFailed: '无法保存首选应用,请检查浏览器存储设置',
|
|
89
|
+
noPreferred: '未选择首选应用',
|
|
90
|
+
previewLoading: '正在读取缩略图…',
|
|
91
|
+
previewUnavailable: '无可用缩略图',
|
|
92
|
+
checking: '正在确认文件…',
|
|
93
|
+
unavailable: '文件服务不可用',
|
|
94
|
+
denied: '没有权限访问该文件',
|
|
95
|
+
probeFailed: '无法确认文件,请检查连接或权限',
|
|
96
|
+
retry: '重试',
|
|
97
|
+
nativeHint: 'Shift+右键 使用原生菜单',
|
|
98
|
+
kinds: {
|
|
99
|
+
text: '文本',
|
|
100
|
+
markdown: 'Markdown',
|
|
101
|
+
html: '网页',
|
|
102
|
+
pdf: 'PDF',
|
|
103
|
+
image: '图片',
|
|
104
|
+
office: 'Office 文档',
|
|
105
|
+
archive: '压缩包',
|
|
106
|
+
video: '视频',
|
|
107
|
+
audio: '音频',
|
|
108
|
+
link: '链接',
|
|
109
|
+
binary: '二进制',
|
|
110
|
+
dir: '文件夹',
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const EN = {
|
|
115
|
+
open: 'Open file',
|
|
116
|
+
openFolder: 'Open folder',
|
|
117
|
+
openIn: (name) => `Open in ${name}`,
|
|
118
|
+
openWith: 'Open with',
|
|
119
|
+
reveal: 'Reveal in File Explorer',
|
|
120
|
+
revealFolder: 'Reveal in File Explorer',
|
|
121
|
+
browser: 'Open in browser',
|
|
122
|
+
copyPath: 'Copy path',
|
|
123
|
+
copyRelPath: 'Copy relative path',
|
|
124
|
+
copyFileName: 'Copy file name',
|
|
125
|
+
copyReference: 'Copy reference',
|
|
126
|
+
copyMarkdown: 'Copy as Markdown',
|
|
127
|
+
copyContents: 'Copy file contents',
|
|
128
|
+
saveAs: 'Save as…',
|
|
129
|
+
copiedPath: 'Path copied',
|
|
130
|
+
copiedRelPath: 'Relative path copied',
|
|
131
|
+
copiedFileName: 'File name copied',
|
|
132
|
+
copiedReference: 'Reference copied',
|
|
133
|
+
copiedMarkdown: 'Markdown copied',
|
|
134
|
+
lineAt: 'line {line}',
|
|
135
|
+
lineRange: 'lines {from}–{to}',
|
|
136
|
+
copiedContents: 'File contents copied',
|
|
137
|
+
copyFailed: 'Copy failed',
|
|
138
|
+
readFailed: 'Read failed',
|
|
139
|
+
tooLarge: 'File is too large — use Save as',
|
|
140
|
+
binary: 'Binary file cannot be copied as text',
|
|
141
|
+
confirmOutsideCopy: 'This file is outside the session workspace and not part of this conversation. Copy its contents to the clipboard?\n{path}',
|
|
142
|
+
openFailed: 'Open failed',
|
|
143
|
+
revealFailed: 'Could not reveal this file',
|
|
144
|
+
popupBlocked: 'The browser blocked the new window',
|
|
145
|
+
noSession: 'No active session',
|
|
146
|
+
loading: 'Reading…',
|
|
147
|
+
dir: 'Folder',
|
|
148
|
+
notFound: 'File not found',
|
|
149
|
+
outsideWorkspace: 'Outside workspace',
|
|
150
|
+
defaultApp: 'Default',
|
|
151
|
+
appsUnit: 'apps',
|
|
152
|
+
openLink: 'Open link',
|
|
153
|
+
copyLink: 'Copy link',
|
|
154
|
+
copyLinkText: 'Copy link text',
|
|
155
|
+
copiedLink: 'Link copied',
|
|
156
|
+
copiedLinkText: 'Link text copied',
|
|
157
|
+
keysMove: '↑↓ select',
|
|
158
|
+
keysRun: 'Enter run',
|
|
159
|
+
keysClose: 'Esc close',
|
|
160
|
+
searchApps: 'Search apps…',
|
|
161
|
+
noMatch: 'No matching app',
|
|
162
|
+
appsLoading: 'Finding applications…',
|
|
163
|
+
appsEmpty: 'No associated applications',
|
|
164
|
+
appsFailed: 'Application lookup failed — retry',
|
|
165
|
+
preferred: 'Preferred',
|
|
166
|
+
pinApp: 'Prefer this application for this file type',
|
|
167
|
+
unpinApp: 'Clear the preferred application for this file type',
|
|
168
|
+
pinHint: 'Shift+Enter to set or clear preference',
|
|
169
|
+
preferenceSaved: 'Preferred application saved',
|
|
170
|
+
preferenceCleared: 'Automatic application selection restored',
|
|
171
|
+
preferenceFailed: 'Could not save preference; check browser storage settings',
|
|
172
|
+
noPreferred: 'No preferred application',
|
|
173
|
+
previewLoading: 'Loading thumbnail…',
|
|
174
|
+
previewUnavailable: 'No thumbnail available',
|
|
175
|
+
checking: 'Checking file…',
|
|
176
|
+
unavailable: 'File service unavailable',
|
|
177
|
+
denied: 'Permission to access this file was denied',
|
|
178
|
+
probeFailed: 'Could not verify file; check connection or permissions',
|
|
179
|
+
retry: 'Retry',
|
|
180
|
+
nativeHint: 'Shift+right-click for the native menu',
|
|
181
|
+
kinds: {
|
|
182
|
+
text: 'Text',
|
|
183
|
+
markdown: 'Markdown',
|
|
184
|
+
html: 'Web page',
|
|
185
|
+
pdf: 'PDF',
|
|
186
|
+
image: 'Image',
|
|
187
|
+
office: 'Office document',
|
|
188
|
+
archive: 'Archive',
|
|
189
|
+
video: 'Video',
|
|
190
|
+
audio: 'Audio',
|
|
191
|
+
link: 'Link',
|
|
192
|
+
binary: 'Binary',
|
|
193
|
+
dir: 'Folder',
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function messages(lang) {
|
|
198
|
+
return /^zh/i.test(String(lang || '')) ? ZH : EN
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ───────────────────────────── path helpers ───────────────────────── */
|
|
202
|
+
|
|
203
|
+
const TEXT_EXT = new Set([
|
|
204
|
+
// documents, data, config
|
|
205
|
+
'txt', 'md', 'markdown', 'mdx', 'rst', 'log', 'csv', 'tsv', 'json', 'jsonc', 'json5', 'jsonl', 'ndjson',
|
|
206
|
+
'har', 'xml', 'yml', 'yaml', 'toml', 'ini', 'cfg', 'conf', 'env', 'properties', 'plist', 'lock',
|
|
207
|
+
// subtitles and captions
|
|
208
|
+
'srt', 'ass', 'ssa', 'vtt', 'sub', 'sbv', 'lrc',
|
|
209
|
+
// web
|
|
210
|
+
'js', 'mjs', 'cjs', 'jsx', 'ts', 'mts', 'cts', 'tsx', 'vue', 'svelte', 'astro', 'html', 'htm', 'xhtml',
|
|
211
|
+
'svg', 'css', 'scss', 'sass', 'less', 'styl', 'hbs', 'ejs', 'pug', 'twig', 'liquid', 'njk',
|
|
212
|
+
// languages
|
|
213
|
+
'py', 'pyi', 'ipynb', 'rb', 'php', 'go', 'rs', 'java', 'kt', 'kts', 'scala', 'groovy',
|
|
214
|
+
'clj', 'cljs', 'ex', 'exs', 'erl', 'hrl', 'hs', 'elm', 'lua', 'pl', 'r', 'jl', 'dart', 'swift', 'm', 'mm',
|
|
215
|
+
'fs', 'fsx', 'cs', 'vb', 'pas', 'f90', 'for', 'asm', 's', 'v', 'sv', 'vhd', 'vhdl',
|
|
216
|
+
'c', 'h', 'cc', 'cpp', 'cxx', 'hpp', 'hh', 'hxx',
|
|
217
|
+
// shells, build, schemas
|
|
218
|
+
'sh', 'bash', 'zsh', 'fish', 'ps1', 'psm1', 'bat', 'cmd', 'sql', 'graphql', 'gql', 'proto', 'thrift', 'prisma',
|
|
219
|
+
'diff', 'patch', 'cmake', 'mk', 'ninja', 'bazel', 'bzl', 'gradle', 'tf', 'tfvars',
|
|
220
|
+
// shaders, typesetting
|
|
221
|
+
'glsl', 'hlsl', 'wgsl', 'frag', 'vert', 'comp', 'shader', 'mtl', 'tex', 'bib', 'rtf',
|
|
222
|
+
])
|
|
223
|
+
const IMAGE_EXT = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'avif', 'tif', 'tiff', 'heic', 'heif', 'jfif'])
|
|
224
|
+
const HTML_EXT = new Set(['html', 'htm', 'xhtml', 'svg'])
|
|
225
|
+
const PDF_EXT = new Set(['pdf'])
|
|
226
|
+
const MD_EXT = new Set(['md', 'markdown', 'mdx'])
|
|
227
|
+
const OFFICE_EXT = new Set(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'rtf', 'pages', 'numbers', 'key'])
|
|
228
|
+
const ARCHIVE_EXT = new Set(['zip', 'tar', 'gz', 'tgz', 'bz2', 'xz', '7z', 'rar', 'zst'])
|
|
229
|
+
const VIDEO_EXT = new Set(['mp4', 'm4v', 'mov', 'webm', 'ogv', 'mkv', 'avi', 'wmv', 'flv', 'mpg', 'mpeg', '3gp', 'mts', 'm2ts'])
|
|
230
|
+
const AUDIO_EXT = new Set(['mp3', 'wav', 'flac', 'm4a', 'aac', 'ogg', 'oga', 'opus', 'wma', 'aiff', 'mid', 'midi'])
|
|
231
|
+
// Chromium can play these in a blob tab; the rest only get external actions.
|
|
232
|
+
const BROWSER_VIDEO = new Set(['mp4', 'm4v', 'mov', 'webm', 'ogv'])
|
|
233
|
+
const BROWSER_AUDIO = new Set(['mp3', 'wav', 'flac', 'm4a', 'ogg', 'oga', 'opus'])
|
|
234
|
+
|
|
235
|
+
const KNOWN_EXT = new Set([...TEXT_EXT, ...IMAGE_EXT, ...PDF_EXT, ...OFFICE_EXT, ...ARCHIVE_EXT, ...VIDEO_EXT, ...AUDIO_EXT,
|
|
236
|
+
// binaries and assets whose names still deserve a menu
|
|
237
|
+
'wasm', 'map', 'lockb', 'db', 'sqlite', 'sqlite3', 'parquet', 'arrow', 'avro', 'orc',
|
|
238
|
+
'woff', 'woff2', 'ttf', 'otf', 'ttc', 'eot', 'fon', 'icns', 'cur',
|
|
239
|
+
'psd', 'ai', 'eps', 'indd', 'fig', 'sketch', 'xd', 'blend', 'fbx', 'obj', 'glb', 'gltf', 'stl',
|
|
240
|
+
'step', 'stp', 'iges', 'igs', '3ds', 'max', 'c4d', 'ma', 'mb', 'abc', 'usd', 'usdz',
|
|
241
|
+
'iso', 'dmg', 'img', 'vhd', 'vhdx', 'vmdk', 'exe', 'msi', 'dll', 'so', 'dylib', 'bin', 'dat', 'pak',
|
|
242
|
+
'class', 'jar', 'war', 'apk', 'ipa', 'deb', 'rpm', 'appimage', 'raw', 'cr2', 'nef', 'arw', 'dng'])
|
|
243
|
+
|
|
244
|
+
const MIME = {
|
|
245
|
+
html: 'text/html;charset=utf-8', htm: 'text/html;charset=utf-8', xhtml: 'application/xhtml+xml',
|
|
246
|
+
svg: 'image/svg+xml', pdf: 'application/pdf', md: 'text/plain;charset=utf-8',
|
|
247
|
+
markdown: 'text/plain;charset=utf-8', mdx: 'text/plain;charset=utf-8',
|
|
248
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp',
|
|
249
|
+
bmp: 'image/bmp', ico: 'image/x-icon', avif: 'image/avif', tif: 'image/tiff', tiff: 'image/tiff',
|
|
250
|
+
json: 'application/json', txt: 'text/plain;charset=utf-8',
|
|
251
|
+
mp4: 'video/mp4', m4v: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm', ogv: 'video/ogg',
|
|
252
|
+
mp3: 'audio/mpeg', wav: 'audio/wav', flac: 'audio/flac', m4a: 'audio/mp4', aac: 'audio/aac',
|
|
253
|
+
ogg: 'audio/ogg', oga: 'audio/ogg', opus: 'audio/ogg',
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** An absolute http(s) URL, or '' for anything else (mailto:, blob:, javascript:). */
|
|
257
|
+
function linkFromHref(href) {
|
|
258
|
+
const value = String(href || '').trim()
|
|
259
|
+
if (!/^https?:\/\//i.test(value)) return ''
|
|
260
|
+
try {
|
|
261
|
+
const url = new URL(value)
|
|
262
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return ''
|
|
263
|
+
return url.href
|
|
264
|
+
} catch {
|
|
265
|
+
return ''
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function isAbsolutePath(value) {
|
|
270
|
+
return /^(?:[A-Za-z]:[\\/]|\\\\[^\\]|\/)/.test(String(value || ''))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizeSlashes(value) {
|
|
274
|
+
return String(value || '').replace(/\\/g, '/')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function basename(value) {
|
|
278
|
+
const s = normalizeSlashes(value).replace(/\/+$/, '')
|
|
279
|
+
const at = s.lastIndexOf('/')
|
|
280
|
+
return at === -1 ? s : s.slice(at + 1)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function extname(value) {
|
|
284
|
+
const name = basename(value)
|
|
285
|
+
const at = name.lastIndexOf('.')
|
|
286
|
+
if (at <= 0) return ''
|
|
287
|
+
return name.slice(at + 1).toLowerCase()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function formatBytes(bytes) {
|
|
291
|
+
const n = Number(bytes)
|
|
292
|
+
if (!Number.isFinite(n) || n < 0) return ''
|
|
293
|
+
if (n < 1024) return `${n} B`
|
|
294
|
+
const units = ['KB', 'MB', 'GB', 'TB']
|
|
295
|
+
let value = n / 1024
|
|
296
|
+
let unit = 0
|
|
297
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
298
|
+
value /= 1024
|
|
299
|
+
unit += 1
|
|
300
|
+
}
|
|
301
|
+
return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** `src/app.ts:12:3` and `src/app.ts#L12-L20` both name one file plus a location. */
|
|
305
|
+
function stripLocationSuffix(value) {
|
|
306
|
+
let path = String(value || '').trim()
|
|
307
|
+
const hash = path.match(/#L(\d+)(?:-L?(\d+))?$/)
|
|
308
|
+
if (hash) {
|
|
309
|
+
return {
|
|
310
|
+
path: path.slice(0, hash.index),
|
|
311
|
+
line: Number(hash[1]),
|
|
312
|
+
endLine: hash[2] === undefined ? undefined : Number(hash[2]),
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const colon = path.match(/:(\d+)(?::(\d+))?$/)
|
|
316
|
+
if (colon) {
|
|
317
|
+
const head = path.slice(0, colon.index)
|
|
318
|
+
// A bare drive prefix (`C:`) is not a location suffix.
|
|
319
|
+
if (!/^[A-Za-z]:$/.test(head)) {
|
|
320
|
+
return {
|
|
321
|
+
path: head,
|
|
322
|
+
line: Number(colon[1]),
|
|
323
|
+
column: colon[2] === undefined ? undefined : Number(colon[2]),
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { path }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function joinPath(cwd, relative) {
|
|
331
|
+
if (!cwd) return relative
|
|
332
|
+
const base = String(cwd).replace(/[\\/]+$/, '')
|
|
333
|
+
const rest = String(relative).replace(/^\.?[\\/]+/, '')
|
|
334
|
+
const sep = /^[A-Za-z]:/.test(base) || base.includes('\\') ? '\\' : '/'
|
|
335
|
+
return `${base}${sep}${rest}`
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Whether one path sits inside a root directory (case-insensitive on Windows). */
|
|
339
|
+
function isInside(root, path) {
|
|
340
|
+
const windows = /^(?:[A-Za-z]:|\\\\|\/\/)/.test(String(root || ''))
|
|
341
|
+
const normalize = (value) => windows ? normalizeSlashes(value).toLowerCase() : normalizeSlashes(value)
|
|
342
|
+
const a = normalize(root || '').replace(/\/+$/, '')
|
|
343
|
+
const b = normalize(path || '')
|
|
344
|
+
if (a === '' && root === '/') return b.startsWith('/')
|
|
345
|
+
if (a === '' || b === '') return false
|
|
346
|
+
return b === a || b.startsWith(`${a}/`)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** The path relative to a root, or '' when it is not inside it. */
|
|
350
|
+
function relativeTo(root, path) {
|
|
351
|
+
const a = normalizeSlashes(root || '').replace(/\/+$/, '')
|
|
352
|
+
const b = normalizeSlashes(path || '')
|
|
353
|
+
if (a === '' && root === '/') return b.startsWith('/') ? b.slice(1) : ''
|
|
354
|
+
if (a === '' || b === '') return ''
|
|
355
|
+
if (!isInside(root, path)) return ''
|
|
356
|
+
if (b.length === a.length) return ''
|
|
357
|
+
const prefix = `${a}/`
|
|
358
|
+
return b.slice(prefix.length)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** A title, an attribute value, or a text token worth resolving as a file. */
|
|
362
|
+
function looksLikePath(value) {
|
|
363
|
+
if (typeof value !== 'string') return false
|
|
364
|
+
const s = value.trim()
|
|
365
|
+
if (s.length === 0 || s.length > 1024) return false
|
|
366
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return false
|
|
367
|
+
if (/[\r\n\t]/.test(s)) return false
|
|
368
|
+
if (/[\\/]/.test(s)) return true
|
|
369
|
+
const ext = extname(s)
|
|
370
|
+
return ext.length > 0 && KNOWN_EXT.has(ext)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function pathFromHref(href) {
|
|
374
|
+
const value = String(href || '').trim()
|
|
375
|
+
if (value === '') return ''
|
|
376
|
+
// The chat, the Sidebar and the delivery table address files as
|
|
377
|
+
// `dsh-resource://file/session/<sessionId>/<path>`; the path is workspace
|
|
378
|
+
// relative or absolute with its drive letter kept literal.
|
|
379
|
+
const resource = /^dsh-resource:\/\/file\/([^?#]*)/i.exec(value)
|
|
380
|
+
if (resource) {
|
|
381
|
+
const match = /^(?:session\/[^/]+|absolute)\/(.+)$/.exec(resource[1])
|
|
382
|
+
if (!match) return ''
|
|
383
|
+
try { return decodeURIComponent(match[1]) } catch { return match[1] }
|
|
384
|
+
}
|
|
385
|
+
if (/^file:\/\//i.test(value)) {
|
|
386
|
+
let path = ''
|
|
387
|
+
try {
|
|
388
|
+
path = decodeURIComponent(value.replace(/^file:\/\//i, ''))
|
|
389
|
+
} catch {
|
|
390
|
+
// A malformed escape (file:///C:/a%zz) must not throw inside the
|
|
391
|
+
// capture-phase listener; fall back to the raw text.
|
|
392
|
+
path = value.replace(/^file:\/\//i, '')
|
|
393
|
+
}
|
|
394
|
+
if (/^\/[A-Za-z]:/.test(path)) path = path.slice(1)
|
|
395
|
+
return path
|
|
396
|
+
}
|
|
397
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return ''
|
|
398
|
+
if (value.startsWith('#')) return ''
|
|
399
|
+
return looksLikePath(value) ? value : ''
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* ─────────────────────────── file classification ─────────────────────────── */
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Extension-less text files are the norm in a repository root (Dockerfile,
|
|
406
|
+
* Makefile, LICENSE, .gitignore). `extname` returns '' for all of them, so
|
|
407
|
+
* without this set they would be classified as binary and lose "copy
|
|
408
|
+
* contents".
|
|
409
|
+
*/
|
|
410
|
+
const TEXT_NAMES = new Set([
|
|
411
|
+
'dockerfile', 'containerfile', 'makefile', 'gnumakefile', 'cmakelists.txt', 'procfile',
|
|
412
|
+
'gemfile', 'rakefile', 'vagrantfile', 'jenkinsfile', 'brewfile', 'justfile', 'taskfile',
|
|
413
|
+
'license', 'licence', 'notice', 'readme', 'changelog', 'changes', 'authors', 'contributors',
|
|
414
|
+
'copying', 'copyright', 'install', 'todo', 'version', 'codeowners', 'editorconfig',
|
|
415
|
+
])
|
|
416
|
+
|
|
417
|
+
/** A dotfile with no second dot is configuration text, not a binary blob. */
|
|
418
|
+
function isTextName(path) {
|
|
419
|
+
const name = basename(path).toLowerCase()
|
|
420
|
+
if (name === '') return false
|
|
421
|
+
if (TEXT_NAMES.has(name)) return true
|
|
422
|
+
// `.gitignore`, `.env`, and also `.env.local` / `.env.production`: a leading
|
|
423
|
+
// dot names a config file, whatever the trailing label looks like.
|
|
424
|
+
return name.startsWith('.')
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The kind decides which rows appear: images get a browser preview, text gets
|
|
429
|
+
* copy-contents, archives and Office files only get the external actions.
|
|
430
|
+
*/
|
|
431
|
+
function classify(path, isDir) {
|
|
432
|
+
if (isDir) return { kind: 'dir', ext: '', canCopyText: false, canBrowser: false, canSaveAs: false }
|
|
433
|
+
const ext = extname(path)
|
|
434
|
+
if (IMAGE_EXT.has(ext)) return { kind: 'image', ext, canCopyText: false, canBrowser: true, canSaveAs: true }
|
|
435
|
+
if (HTML_EXT.has(ext)) return { kind: 'html', ext, canCopyText: true, canBrowser: true, canSaveAs: true }
|
|
436
|
+
if (PDF_EXT.has(ext)) return { kind: 'pdf', ext, canCopyText: false, canBrowser: true, canSaveAs: true }
|
|
437
|
+
if (MD_EXT.has(ext)) return { kind: 'markdown', ext, canCopyText: true, canBrowser: true, canSaveAs: true }
|
|
438
|
+
if (OFFICE_EXT.has(ext)) return { kind: 'office', ext, canCopyText: false, canBrowser: false, canSaveAs: true }
|
|
439
|
+
if (ARCHIVE_EXT.has(ext)) return { kind: 'archive', ext, canCopyText: false, canBrowser: false, canSaveAs: true }
|
|
440
|
+
if (VIDEO_EXT.has(ext)) return { kind: 'video', ext, canCopyText: false, canBrowser: BROWSER_VIDEO.has(ext), canSaveAs: true }
|
|
441
|
+
if (AUDIO_EXT.has(ext)) return { kind: 'audio', ext, canCopyText: false, canBrowser: BROWSER_AUDIO.has(ext), canSaveAs: true }
|
|
442
|
+
if (TEXT_EXT.has(ext)) return { kind: 'text', ext, canCopyText: true, canBrowser: false, canSaveAs: true }
|
|
443
|
+
if (isTextName(path)) return { kind: 'text', ext, canCopyText: true, canBrowser: false, canSaveAs: true }
|
|
444
|
+
return { kind: 'binary', ext, canCopyText: false, canBrowser: false, canSaveAs: true }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function mimeOf(path) {
|
|
448
|
+
const ext = extname(path)
|
|
449
|
+
if (MIME[ext]) return MIME[ext]
|
|
450
|
+
if (TEXT_EXT.has(ext) || isTextName(path)) return 'text/plain;charset=utf-8'
|
|
451
|
+
return 'application/octet-stream'
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const EDITOR_RE = /(visual studio code|vs ?code|vscode|cursor|windsurf|zed|sublime|intellij|pycharm|webstorm|goland|rider|phpstorm|android studio|notepad\+\+|vim|neovim|emacs|textmate|xcode)/i
|
|
455
|
+
|
|
456
|
+
function pickEditor(apps) {
|
|
457
|
+
if (!Array.isArray(apps)) return null
|
|
458
|
+
return apps.find((app) => app && typeof app.name === 'string' && EDITOR_RE.test(app.name)) || null
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function orderApplications(apps, preferredId) {
|
|
462
|
+
const seen = new Set()
|
|
463
|
+
const rank = (app) => app.id === preferredId ? 0 : app.default ? 1 : EDITOR_RE.test(app.name) ? 2 : 3
|
|
464
|
+
return (Array.isArray(apps) ? apps : [])
|
|
465
|
+
.filter((app) => {
|
|
466
|
+
if (!app || typeof app.id !== 'string' || typeof app.name !== 'string' || seen.has(app.id)) return false
|
|
467
|
+
seen.add(app.id)
|
|
468
|
+
return true
|
|
469
|
+
})
|
|
470
|
+
.map((app, index) => ({ app, index }))
|
|
471
|
+
.sort((a, b) => rank(a.app) - rank(b.app) || a.index - b.index)
|
|
472
|
+
.map(({ app }) => app)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Store application IDs only, never commands. Host validation still owns opens.
|
|
476
|
+
function applicationPreferences(storage) {
|
|
477
|
+
const key = 'dsh-file-actions.preferred-apps.v1'
|
|
478
|
+
const type = (path) => extname(path).toLowerCase() || '[no extension]'
|
|
479
|
+
const read = () => {
|
|
480
|
+
try {
|
|
481
|
+
const value = JSON.parse(storage.getItem(key) || '{}')
|
|
482
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
483
|
+
} catch { return {} }
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
get(path) { const value = read()[type(path)]; return typeof value === 'string' ? value : '' },
|
|
487
|
+
set(path, appId) {
|
|
488
|
+
try {
|
|
489
|
+
const value = read()
|
|
490
|
+
if (appId) Object.defineProperty(value, type(path), { value: appId, enumerable: true, configurable: true, writable: true })
|
|
491
|
+
else delete value[type(path)]
|
|
492
|
+
storage.setItem(key, JSON.stringify(value))
|
|
493
|
+
return true
|
|
494
|
+
} catch { return false }
|
|
495
|
+
},
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function buildStatusModel(path, message, t, retry = false) {
|
|
500
|
+
return { title: basename(path) || path, subtitle: path, pathLine: path, kind: 'binary',
|
|
501
|
+
status: true, sections: [[{ id: 'status', label: message, disabled: true }],
|
|
502
|
+
...(retry ? [[{ id: 'retry', label: t.retry, action: 'retry', icon: 'open' }]] : [])] }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* ─────────────────────────── menu model (pure) ─────────────────────────── */
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Build the rows as data so the widget stays a dumb renderer and the tests can
|
|
509
|
+
* assert which actions a file kind earns.
|
|
510
|
+
* @param input - resolved target: absolute path, display path, kind flags, apps.
|
|
511
|
+
* @returns header text plus ordered sections of rows.
|
|
512
|
+
*/
|
|
513
|
+
/**
|
|
514
|
+
* A reference like `src/app.ts:42:7` or `notes.md#L42-L48` names a line. The
|
|
515
|
+
* Host cannot position an editor, but showing and copying the location still
|
|
516
|
+
* answers "which part of the file" while the menu is open.
|
|
517
|
+
*/
|
|
518
|
+
function formatLocation(location, t) {
|
|
519
|
+
const line = location && Number(location.line)
|
|
520
|
+
if (!line) return ''
|
|
521
|
+
const end = Number(location && location.endLine)
|
|
522
|
+
if (end && end !== line) return t.lineRange.replace('{from}', line).replace('{to}', end)
|
|
523
|
+
return t.lineAt.replace('{line}', line)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function buildMenuModel(input) {
|
|
527
|
+
const t = input.t || ZH
|
|
528
|
+
const { absolutePath, displayPath, isDir, bytes } = input
|
|
529
|
+
const apps = orderApplications(input.apps, input.preferredId)
|
|
530
|
+
const file = classify(absolutePath, isDir)
|
|
531
|
+
const sections = []
|
|
532
|
+
const primary = []
|
|
533
|
+
|
|
534
|
+
primary.push({
|
|
535
|
+
id: 'open',
|
|
536
|
+
label: isDir ? t.openFolder : t.open,
|
|
537
|
+
icon: isDir ? 'folder' : 'open',
|
|
538
|
+
action: 'open',
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
const editor = apps.find((app) => app.id === input.preferredId) || pickEditor(apps)
|
|
542
|
+
if (!isDir && (editor || input.appsState)) {
|
|
543
|
+
primary.push({
|
|
544
|
+
id: 'editor',
|
|
545
|
+
label: editor ? t.openIn(editor.name) : input.appsState === 'loading' ? t.appsLoading : t.noPreferred,
|
|
546
|
+
icon: 'editor',
|
|
547
|
+
action: 'open',
|
|
548
|
+
appId: editor?.id,
|
|
549
|
+
disabled: !editor,
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!isDir && (apps.length > 0 || input.appsState)) {
|
|
554
|
+
primary.push({
|
|
555
|
+
id: 'openWith',
|
|
556
|
+
label: input.appsState === 'failed' ? t.appsFailed : input.appsState === 'ready' && !apps.length ? t.appsEmpty : t.openWith,
|
|
557
|
+
icon: 'chevron',
|
|
558
|
+
action: input.appsState === 'failed' ? 'retryApps' : undefined,
|
|
559
|
+
keepOpen: input.appsState === 'failed',
|
|
560
|
+
disabled: apps.length === 0 && input.appsState !== 'failed',
|
|
561
|
+
children: apps.length ? apps.map((app) => ({
|
|
562
|
+
id: `app:${app.id}`,
|
|
563
|
+
label: app.name,
|
|
564
|
+
action: 'open',
|
|
565
|
+
appId: app.id,
|
|
566
|
+
iconData: app.icon || null,
|
|
567
|
+
checked: app.default === true,
|
|
568
|
+
preferred: app.id === input.preferredId,
|
|
569
|
+
pinnable: true,
|
|
570
|
+
})) : undefined,
|
|
571
|
+
})
|
|
572
|
+
}
|
|
573
|
+
if (primary.length > 0) sections.push(primary)
|
|
574
|
+
|
|
575
|
+
const navigate = []
|
|
576
|
+
if (!isDir && file.canBrowser) {
|
|
577
|
+
navigate.push({
|
|
578
|
+
id: 'browser',
|
|
579
|
+
label: t.browser,
|
|
580
|
+
icon: 'globe',
|
|
581
|
+
action: 'browser',
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
navigate.push({
|
|
585
|
+
id: 'reveal',
|
|
586
|
+
label: isDir ? t.revealFolder : t.reveal,
|
|
587
|
+
icon: 'reveal',
|
|
588
|
+
action: 'reveal',
|
|
589
|
+
})
|
|
590
|
+
sections.push(navigate)
|
|
591
|
+
|
|
592
|
+
const transfer = [
|
|
593
|
+
{ id: 'copyPath', label: t.copyPath, icon: 'copy', action: 'copyPath' },
|
|
594
|
+
]
|
|
595
|
+
if (displayPath && normalizeSlashes(displayPath) !== normalizeSlashes(absolutePath)) {
|
|
596
|
+
transfer.push({ id: 'copyRelPath', label: t.copyRelPath, icon: 'copy', action: 'copyRelPath' })
|
|
597
|
+
}
|
|
598
|
+
if (!isDir) {
|
|
599
|
+
transfer.push({ id: 'copyFileName', label: t.copyFileName, icon: 'copy', action: 'copyFileName' })
|
|
600
|
+
}
|
|
601
|
+
// Copying the reference verbatim is only useful when it carries a location.
|
|
602
|
+
if (!isDir && input.location && Number(input.location.line) && input.reference) {
|
|
603
|
+
transfer.push({ id: 'copyReference', label: t.copyReference, icon: 'clipboard', action: 'copyReference' })
|
|
604
|
+
}
|
|
605
|
+
if (!isDir && file.canCopyText) {
|
|
606
|
+
transfer.push({ id: 'copyContents', label: t.copyContents, icon: 'clipboard', action: 'copyContents' })
|
|
607
|
+
}
|
|
608
|
+
if (!isDir) {
|
|
609
|
+
transfer.push({ id: 'saveAs', label: t.saveAs, icon: 'download', action: 'saveAs' })
|
|
610
|
+
}
|
|
611
|
+
sections.push(transfer)
|
|
612
|
+
|
|
613
|
+
const kindLabel = t.kinds[file.kind] || t.kinds.binary
|
|
614
|
+
const locationLabel = formatLocation(input.location, t)
|
|
615
|
+
const metaParts = locationLabel ? [locationLabel, kindLabel] : [kindLabel]
|
|
616
|
+
if (!isDir && bytes) metaParts.push(formatBytes(bytes))
|
|
617
|
+
// Copy/save-as read through the Host, which also serves paths outside the
|
|
618
|
+
// session workspace. Say so instead of letting the absolute path be the only
|
|
619
|
+
// clue that the file is not part of this project.
|
|
620
|
+
if (input.outsideWorkspace) metaParts.push(t.outsideWorkspace)
|
|
621
|
+
const meta = metaParts.join(' · ')
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
title: basename(absolutePath) || absolutePath,
|
|
625
|
+
subtitle: absolutePath,
|
|
626
|
+
// Inside the workspace the relative path answers "which one"; outside it,
|
|
627
|
+
// the absolute path is the only thing that does.
|
|
628
|
+
pathLine: input.outsideWorkspace
|
|
629
|
+
? absolutePath
|
|
630
|
+
: (relativeTo(input.cwd, absolutePath) || displayPath || absolutePath),
|
|
631
|
+
sizeLabel: !isDir && bytes ? formatBytes(bytes) : '',
|
|
632
|
+
kindLabel,
|
|
633
|
+
locationLabel,
|
|
634
|
+
meta,
|
|
635
|
+
kind: file.kind,
|
|
636
|
+
preview: file.kind === 'image' || extname(absolutePath) === 'svg',
|
|
637
|
+
outsideWorkspace: input.outsideWorkspace === true,
|
|
638
|
+
sections: sections.filter((section) => section.length > 0),
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* The same menu shape for a web link: the header names the site and the path,
|
|
644
|
+
* and the rows act on the URL instead of a file.
|
|
645
|
+
*/
|
|
646
|
+
function buildLinkMenuModel(input) {
|
|
647
|
+
const t = input.t || ZH
|
|
648
|
+
let host = input.url
|
|
649
|
+
let pathLine = input.url
|
|
650
|
+
try {
|
|
651
|
+
const url = new URL(input.url)
|
|
652
|
+
host = url.hostname.replace(/^www\./i, '') || input.url
|
|
653
|
+
const path = `${url.pathname}${url.search}`.replace(/\/$/, '')
|
|
654
|
+
pathLine = path === '' ? '/' : path
|
|
655
|
+
} catch { /* keep the raw URL */ }
|
|
656
|
+
const copy = [
|
|
657
|
+
{ id: 'copyLink', label: t.copyLink, icon: 'copy', action: 'copyLink' },
|
|
658
|
+
{ id: 'copyLinkText', label: t.copyLinkText, icon: 'clipboard', action: 'copyLinkText' },
|
|
659
|
+
]
|
|
660
|
+
// `[text](url)` only exists when the link has visible text to label it.
|
|
661
|
+
if (String(input.text || '').trim() !== '') {
|
|
662
|
+
copy.push({ id: 'copyMarkdown', label: t.copyMarkdown, icon: 'markdown', action: 'copyMarkdown' })
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
title: host,
|
|
666
|
+
subtitle: input.url,
|
|
667
|
+
pathLine,
|
|
668
|
+
sizeLabel: '',
|
|
669
|
+
kindLabel: t.kinds.link,
|
|
670
|
+
meta: t.kinds.link,
|
|
671
|
+
kind: 'link',
|
|
672
|
+
outsideWorkspace: false,
|
|
673
|
+
sections: [
|
|
674
|
+
[{ id: 'openLink', label: t.openLink, icon: 'globe', action: 'openLink' }],
|
|
675
|
+
copy,
|
|
676
|
+
],
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/* ─────────────────────────────── icons ─────────────────────────────── */
|
|
681
|
+
|
|
682
|
+
const ICON_PATHS = {
|
|
683
|
+
open: '<path d="M9.2 2.6H4.4a1 1 0 0 0-1 1v8.8a1 1 0 0 0 1 1h7.2a1 1 0 0 0 1-1V6.2z"/><path d="M9.2 2.6v3.6h3.4"/>',
|
|
684
|
+
folder: '<path d="M2.5 5.5A1.5 1.5 0 0 1 4 4h3l1.4 1.5H12a1.5 1.5 0 0 1 1.5 1.5v4.5A1.5 1.5 0 0 1 12 13H4a1.5 1.5 0 0 1-1.5-1.5z"/>',
|
|
685
|
+
editor: '<path d="M5.8 4.6 2.8 8l3 3.4"/><path d="M10.2 4.6 13.2 8l-3 3.4"/>',
|
|
686
|
+
chevron: '<path d="M6.5 4.5 10 8l-3.5 3.5"/>',
|
|
687
|
+
reveal: '<path d="M2.5 4.5h5l1.3 1.6H13a1 1 0 0 1 1 1v5.4a1 1 0 0 1-1 1H3.5a1 1 0 0 1-1-1z"/><path d="M6 10.4h4"/>',
|
|
688
|
+
copy: '<rect x="5.5" y="5.5" width="7" height="8" rx="1.2"/><path d="M10.5 3.6V3a1 1 0 0 0-1-1H3.9a1 1 0 0 0-1 1v7.2a1 1 0 0 0 1 1h.6"/>',
|
|
689
|
+
clipboard: '<rect x="4.5" y="3.5" width="7" height="10" rx="1.3"/><path d="M6.6 3.5V2.8h2.8v.7"/><path d="M6.4 7h3.2"/><path d="M6.4 9.4h3.2"/>',
|
|
690
|
+
download: '<path d="M8 2.8v6.4"/><path d="M5.6 7.2 8 9.6l2.4-2.4"/><path d="M3.2 11.4v1.2a1 1 0 0 0 1 1h7.6a1 1 0 0 0 1-1v-1.2"/>',
|
|
691
|
+
globe: '<circle cx="8" cy="8" r="5.4"/><path d="M2.8 8h10.4"/><path d="M8 2.6c1.6 1.6 2.4 3.4 2.4 5.4S9.6 11.8 8 13.4C6.4 11.8 5.6 10 5.6 8S6.4 4.2 8 2.6"/>',
|
|
692
|
+
check: '<path d="M3.6 8.4 6.6 11.4 12.4 4.9"/>',
|
|
693
|
+
link: '<path d="M6.6 9.4a2.6 2.6 0 0 0 3.7 0l1.9-1.9a2.6 2.6 0 0 0-3.7-3.7l-1 1"/><path d="M9.4 6.6a2.6 2.6 0 0 0-3.7 0L3.8 8.5a2.6 2.6 0 0 0 3.7 3.7l1-1"/>',
|
|
694
|
+
// File-type glyphs for the header tile: shape carries the kind, the tile hue
|
|
695
|
+
// only reinforces it.
|
|
696
|
+
text: '<path d="M4 4.6h8"/><path d="M4 7.6h8"/><path d="M4 10.6h5"/>',
|
|
697
|
+
markdown: '<path d="M2.8 10.6V5.4l2.1 2.5 2.1-2.5v5.2"/><path d="M11.1 5.6v3.4"/><path d="M9.6 7.6l1.5 1.5 1.5-1.5"/>',
|
|
698
|
+
image: '<rect x="2.6" y="3.6" width="10.8" height="8.8" rx="1.4"/><circle cx="6" cy="6.6" r="1"/><path d="M3.6 11.2 6.9 8.3l1.9 1.7 1.8-1.4 1.9 2.1"/>',
|
|
699
|
+
web: '<rect x="2.6" y="3.4" width="10.8" height="9.2" rx="1.4"/><path d="M2.6 6.2h10.8"/><circle cx="4.6" cy="4.8" r=".5" fill="currentColor" stroke="none"/><circle cx="6.2" cy="4.8" r=".5" fill="currentColor" stroke="none"/>',
|
|
700
|
+
doc: '<path d="M9.2 2.6H4.4a1 1 0 0 0-1 1v8.8a1 1 0 0 0 1 1h7.2a1 1 0 0 0 1-1V6.2z"/><path d="M9.2 2.6v3.6h3.4"/>',
|
|
701
|
+
office: '<rect x="3.4" y="2.8" width="9.2" height="10.4" rx="1.3"/><path d="M6 6h4"/><path d="M6 8.3h4"/><path d="M6 10.6h2.4"/>',
|
|
702
|
+
archive: '<rect x="3" y="4.8" width="10" height="7.4" rx="1.2"/><path d="M3 7.5h10"/><path d="M6.6 4.8V3.4h2.8v1.4"/>',
|
|
703
|
+
binary: '<rect x="3" y="3.4" width="10" height="9.2" rx="1.2"/><path d="M5.6 6.6h1.1v3.2"/><path d="M9.3 6.6h1.5l-1.5 3.2h1.5"/>',
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** The header tile glyph for one file kind. */
|
|
707
|
+
const KIND_ICONS = {
|
|
708
|
+
text: 'text',
|
|
709
|
+
markdown: 'markdown',
|
|
710
|
+
html: 'web',
|
|
711
|
+
pdf: 'doc',
|
|
712
|
+
image: 'image',
|
|
713
|
+
office: 'office',
|
|
714
|
+
archive: 'archive',
|
|
715
|
+
binary: 'binary',
|
|
716
|
+
dir: 'folder',
|
|
717
|
+
link: 'link',
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function iconSvg(name) {
|
|
721
|
+
const body = ICON_PATHS[name] || ICON_PATHS.open
|
|
722
|
+
return `<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/* ─────────────────────────────── styles ─────────────────────────────── */
|
|
726
|
+
|
|
727
|
+
const CSS = `
|
|
728
|
+
.dfa-pin{flex:none;margin-left:6px;border:0;border-radius:4px;background:transparent;color:inherit;
|
|
729
|
+
font:inherit;cursor:pointer;padding:0 5px;line-height:1.6}
|
|
730
|
+
.dfa-pin:hover,.dfa-pin:focus-visible{background:rgba(127,127,127,.22);outline:1px solid currentColor}
|
|
731
|
+
.dfa-pin[aria-pressed="true"]{color:#5b8cff}
|
|
732
|
+
|
|
733
|
+
.dfa-menu{position:fixed;z-index:2147483000;width:min(24em,calc(100vw - 16px));min-width:0;max-width:calc(100vw - 16px);box-sizing:border-box;
|
|
734
|
+
display:flex;flex-direction:column;max-height:min(78vh,560px);
|
|
735
|
+
border-radius:var(--dsw-radius-lg,10px);border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.32));
|
|
736
|
+
/* Readability floor: an almost-opaque base colour sits under the themed layer.
|
|
737
|
+
A translucent theme (liquid glass over a wallpaper) keeps its tint, but text
|
|
738
|
+
never lands on the wallpaper itself. The exact colour is computed at open
|
|
739
|
+
time from the theme's own background; Canvas is the pre-JS fallback. */
|
|
740
|
+
background-color:var(--dfa-floor,Canvas);
|
|
741
|
+
background-image:linear-gradient(var(--dsw-alias-bg-layer-1,transparent),var(--dsw-alias-bg-layer-1,transparent));
|
|
742
|
+
color:var(--dsw-alias-label-primary,#e8e8e8);
|
|
743
|
+
box-shadow:0 1px 2px rgba(0,0,0,.14),0 14px 38px rgba(0,0,0,.32);
|
|
744
|
+
font-family:var(--dsw-font-family,system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif);
|
|
745
|
+
font-size:max(13px,.875rem);line-height:1.5;-webkit-font-smoothing:antialiased;user-select:none;outline:none;
|
|
746
|
+
animation:dfa-in 90ms cubic-bezier(.2,.7,.3,1) both}
|
|
747
|
+
/* An explicit display on a row beats the UA [hidden] rule, so filtered rows need
|
|
748
|
+
this to actually disappear. */
|
|
749
|
+
.dfa-menu [hidden]{display:none!important}
|
|
750
|
+
@keyframes dfa-in{from{opacity:0}to{opacity:1}}
|
|
751
|
+
@media (prefers-reduced-motion:reduce){.dfa-menu{animation:none}.dfa-item{transition:none}}
|
|
752
|
+
/* The body is the scroll container so the fades can sit on its edges without
|
|
753
|
+
fading the menu's own border and shadow. */
|
|
754
|
+
.dfa-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable;
|
|
755
|
+
padding:5px;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-border-l2,rgba(127,127,127,.4)) transparent}
|
|
756
|
+
.dfa-body[data-fade="both"]{mask-image:linear-gradient(to bottom,transparent 0,#000 16px,#000 calc(100% - 16px),transparent 100%)}
|
|
757
|
+
.dfa-body[data-fade="top"]{mask-image:linear-gradient(to bottom,transparent 0,#000 16px)}
|
|
758
|
+
.dfa-body[data-fade="bottom"]{mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent 100%)}
|
|
759
|
+
.dfa-thumb{height:118px;box-sizing:content-box;color:var(--dsw-alias-label-tertiary,#888);font-size:.85em;margin:-5px -5px 6px;padding:9px;display:grid;place-items:center;
|
|
760
|
+
background:color-mix(in srgb,var(--dsw-static-neutral-00,#fff) 6%,transparent);
|
|
761
|
+
border-bottom:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.28))}
|
|
762
|
+
.dfa-thumb img{max-width:100%;max-height:118px;display:block;object-fit:contain;border-radius:6px}
|
|
763
|
+
.dfa-head{display:flex;align-items:center;gap:9px;padding:6px 7px 8px;min-width:0;
|
|
764
|
+
border-bottom:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.28));margin-bottom:3px}
|
|
765
|
+
.dfa-tile{flex:none;width:28px;height:28px;border-radius:8px;display:grid;place-items:center;
|
|
766
|
+
color:var(--dfa-hue,#5b8cff);
|
|
767
|
+
background:color-mix(in srgb,var(--dfa-hue,#5b8cff) 13%,transparent);
|
|
768
|
+
box-shadow:inset 0 0 0 .5px color-mix(in srgb,var(--dfa-hue,#5b8cff) 30%,transparent)}
|
|
769
|
+
.dfa-tile svg{width:16px;height:16px}
|
|
770
|
+
.dfa-tile[data-kind="text"]{--dfa-hue:#5b8cff}
|
|
771
|
+
.dfa-tile[data-kind="markdown"]{--dfa-hue:#7b8cff}
|
|
772
|
+
.dfa-tile[data-kind="html"]{--dfa-hue:#e0714a}
|
|
773
|
+
.dfa-tile[data-kind="pdf"]{--dfa-hue:#e5484d}
|
|
774
|
+
.dfa-tile[data-kind="image"]{--dfa-hue:#2fa46b}
|
|
775
|
+
.dfa-tile[data-kind="office"]{--dfa-hue:#2f6fdb}
|
|
776
|
+
.dfa-tile[data-kind="archive"]{--dfa-hue:#c9971f}
|
|
777
|
+
.dfa-tile[data-kind="binary"]{--dfa-hue:#8b8d98}
|
|
778
|
+
.dfa-tile[data-kind="dir"]{--dfa-hue:#c9971f}
|
|
779
|
+
.dfa-tile[data-kind="link"]{--dfa-hue:#4f8cff}
|
|
780
|
+
.dfa-headtext{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
|
|
781
|
+
.dfa-name{font-weight:600;font-size:1em;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
782
|
+
.dfa-path{font-family:var(--ds-font-family-code,Consolas,monospace);font-size:.85em;line-height:1.4;
|
|
783
|
+
color:var(--dsw-alias-label-tertiary,rgba(160,160,160,.95));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
784
|
+
.dfa-loc{flex:none;align-self:flex-start;margin-top:2px;padding:0 6px;border-radius:999px;font-size:.85em;line-height:1.4;
|
|
785
|
+
background:color-mix(in srgb,var(--dsw-alias-label-primary,#e8e8e8) 10%,transparent);
|
|
786
|
+
color:var(--dsw-alias-label-secondary,#a9adb5)}
|
|
787
|
+
.dfa-size{flex:none;align-self:flex-start;padding-top:2px;font-size:.85em;line-height:1.4;
|
|
788
|
+
color:var(--dsw-alias-label-tertiary,rgba(160,160,160,.95));font-variant-numeric:tabular-nums}
|
|
789
|
+
.dfa-aside{flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:2px}
|
|
790
|
+
.dfa-badge{flex:none;align-self:flex-start;margin-top:2px;font-size:.85em;line-height:1.4;padding:0 6px;border-radius:999px;
|
|
791
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.4));color:var(--dsw-alias-label-tertiary,rgba(160,160,160,.95))}
|
|
792
|
+
.dfa-sep{height:1px;margin:5px 7px;background:var(--dsw-alias-border-l2,rgba(127,127,127,.28))}
|
|
793
|
+
.dfa-item{display:flex;align-items:center;gap:10px;width:100%;box-sizing:border-box;min-height:30px;padding:4px 8px;
|
|
794
|
+
border:0;border-radius:6px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;
|
|
795
|
+
transition:background-color 70ms ease}
|
|
796
|
+
.dfa-item[data-focus="true"]{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.18))}
|
|
797
|
+
.dfa-menu[data-keyboard="true"] .dfa-item[data-focus="true"]{
|
|
798
|
+
box-shadow:inset 0 0 0 1.5px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#4f8cff))}
|
|
799
|
+
.dfa-item[aria-disabled="true"]{opacity:.45;cursor:default}
|
|
800
|
+
.dfa-ico{flex:none;width:16px;height:16px;display:flex;align-items:center;justify-content:center;
|
|
801
|
+
color:var(--dsw-alias-label-secondary,rgba(190,190,190,.95))}
|
|
802
|
+
.dfa-ico img{width:16px;height:16px;object-fit:contain;display:block;border-radius:3px}
|
|
803
|
+
.dfa-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
804
|
+
.dfa-tail{flex:none;display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-tertiary,rgba(160,160,160,.95));font-size:.85em}
|
|
805
|
+
.dfa-default{font-size:.85em;line-height:1.4;padding:0 6px;border-radius:999px;
|
|
806
|
+
color:var(--dsw-alias-state-business-primary,#4f8cff);
|
|
807
|
+
background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4f8cff) 14%,transparent)}
|
|
808
|
+
.dfa-sub{position:fixed;z-index:2147483001;max-height:min(64vh,440px)}
|
|
809
|
+
.dfa-subhead{position:sticky;top:-5px;z-index:2;display:flex;align-items:center;justify-content:space-between;gap:8px;
|
|
810
|
+
margin:-5px -5px 0;padding:6px 13px 5px;background:var(--dsw-alias-bg-layer-1,#24262b);
|
|
811
|
+
border-bottom:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.28));
|
|
812
|
+
font-size:.85em;line-height:1.4;color:var(--dsw-alias-label-tertiary,rgba(160,160,160,.95))}
|
|
813
|
+
.dfa-searchwrap{position:sticky;top:27px;z-index:1;margin:0 -5px 4px;padding:5px 8px 6px;
|
|
814
|
+
background:var(--dsw-alias-bg-layer-1,#24262b);border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.2))}
|
|
815
|
+
.dfa-search{width:100%;box-sizing:border-box;font:inherit;font-size:1em;line-height:1.4;padding:3px 7px;
|
|
816
|
+
border-radius:5px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.4));
|
|
817
|
+
background:var(--dsw-alias-bg-layer-2,transparent);color:inherit;outline:none}
|
|
818
|
+
.dfa-search:focus{border-color:var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#4f8cff))}
|
|
819
|
+
.dfa-search::placeholder{color:var(--dsw-alias-label-tertiary,rgba(160,160,160,.95))}
|
|
820
|
+
.dfa-empty{padding:8px 10px;font-size:.85em;color:var(--dsw-alias-label-tertiary,rgba(160,160,160,.95))}
|
|
821
|
+
.dfa-foot{display:flex;justify-content:center;gap:12px;margin-top:5px;padding:6px 8px 2px;
|
|
822
|
+
border-top:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.28));
|
|
823
|
+
font-size:.85em;line-height:1.4;color:var(--dsw-alias-label-tertiary,rgba(160,160,160,.95))}
|
|
824
|
+
.dfa-toast{position:fixed;left:50%;bottom:32px;transform:translateX(-50%);z-index:2147483002;
|
|
825
|
+
padding:7px 14px;border-radius:8px;font-size:.875rem;line-height:1.4;pointer-events:none;
|
|
826
|
+
background-color:var(--dfa-floor,Canvas);
|
|
827
|
+
background-image:linear-gradient(var(--dsw-alias-bg-layer-2,transparent),var(--dsw-alias-bg-layer-2,transparent));
|
|
828
|
+
color:var(--dsw-alias-label-primary,#eee);
|
|
829
|
+
box-shadow:0 2px 6px rgba(0,0,0,.18),0 10px 28px rgba(0,0,0,.28);
|
|
830
|
+
opacity:0;transition:opacity .16s ease}
|
|
831
|
+
.dfa-toast[data-show="true"]{opacity:1}
|
|
832
|
+
.dfa-toast[data-error="true"]{color:var(--dsw-alias-state-error-primary,#f85149)}
|
|
833
|
+
`
|
|
834
|
+
|
|
835
|
+
function ensureCss() {
|
|
836
|
+
if (typeof document === 'undefined') return
|
|
837
|
+
if (document.querySelector('style[data-plugin-css="dsh-file-actions"]')) return
|
|
838
|
+
const tag = document.createElement('style')
|
|
839
|
+
tag.dataset.plugin = 'dsh-file-actions'
|
|
840
|
+
tag.dataset.pluginCss = 'dsh-file-actions'
|
|
841
|
+
tag.textContent = CSS
|
|
842
|
+
document.head.appendChild(tag)
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* A near-opaque colour derived from the theme's own page background. A theme
|
|
847
|
+
* that makes every surface translucent (liquid glass over a wallpaper) would
|
|
848
|
+
* otherwise let the wallpaper through and drop the menu's contrast. Recomputed
|
|
849
|
+
* per open so a theme switch is picked up without a reload.
|
|
850
|
+
*/
|
|
851
|
+
function menuFloor() {
|
|
852
|
+
if (typeof document === 'undefined') return ''
|
|
853
|
+
try {
|
|
854
|
+
const probe = document.createElement('div')
|
|
855
|
+
// Derive the floor from the same surface colour the menu paints, so a dark
|
|
856
|
+
// glass theme gets a dark floor instead of a white one.
|
|
857
|
+
probe.style.cssText = 'position:fixed;left:-9999px;top:0;width:1px;height:1px;background:var(--dsw-alias-bg-layer-1,#24262b)'
|
|
858
|
+
document.body.appendChild(probe)
|
|
859
|
+
const raw = getComputedStyle(probe).backgroundColor
|
|
860
|
+
probe.remove()
|
|
861
|
+
const match = /rgba?\(([^)]+)\)/.exec(raw)
|
|
862
|
+
if (!match) return ''
|
|
863
|
+
const parts = match[1].split(',').map((value) => parseFloat(value))
|
|
864
|
+
const [r, g, b] = parts
|
|
865
|
+
const alpha = parts.length > 3 ? parts[3] : 1
|
|
866
|
+
if (![r, g, b, alpha].every((value) => Number.isFinite(value))) return ''
|
|
867
|
+
// Flatten the theme colour against black or white by its own luminance, then
|
|
868
|
+
// keep a hint of transparency so a glass theme still reads as glass.
|
|
869
|
+
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
|
|
870
|
+
const base = luminance < 0.5 ? 0 : 255
|
|
871
|
+
const blend = (channel) => Math.round(channel * alpha + base * (1 - alpha))
|
|
872
|
+
return `rgba(${blend(r)}, ${blend(g)}, ${blend(b)}, 0.94)`
|
|
873
|
+
} catch {
|
|
874
|
+
return ''
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/* ─────────────────────────────── toast ─────────────────────────────── */
|
|
879
|
+
|
|
880
|
+
let toastTimer = null
|
|
881
|
+
function toast(text, options) {
|
|
882
|
+
if (typeof document === 'undefined' || !text) return
|
|
883
|
+
let node = document.querySelector('.dfa-toast')
|
|
884
|
+
if (!node) {
|
|
885
|
+
node = document.createElement('div')
|
|
886
|
+
node.className = 'dfa-toast'
|
|
887
|
+
const floor = menuFloor()
|
|
888
|
+
if (floor) node.style.setProperty('--dfa-floor', floor)
|
|
889
|
+
document.body.appendChild(node)
|
|
890
|
+
}
|
|
891
|
+
node.textContent = text
|
|
892
|
+
node.dataset.error = options && options.error ? 'true' : 'false'
|
|
893
|
+
// Force a frame so a repeated message re-runs the fade-in.
|
|
894
|
+
node.dataset.show = 'false'
|
|
895
|
+
requestAnimationFrame(() => {
|
|
896
|
+
node.dataset.show = 'true'
|
|
897
|
+
})
|
|
898
|
+
if (toastTimer) clearTimeout(toastTimer)
|
|
899
|
+
toastTimer = setTimeout(() => {
|
|
900
|
+
node.dataset.show = 'false'
|
|
901
|
+
}, options && options.error ? 3200 : 1800)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/* ─────────────────────────────── bridge ─────────────────────────────── */
|
|
905
|
+
|
|
906
|
+
/** Every Remote call answers `{ ok, value }`; unwrap or return null. */
|
|
907
|
+
function unwrap(result) {
|
|
908
|
+
if (!result || typeof result !== 'object') return null
|
|
909
|
+
if (result.ok === true) return result.value
|
|
910
|
+
return null
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function probeError(error) {
|
|
914
|
+
const code = String(error?.code || '')
|
|
915
|
+
if (/not-found|ENOENT/.test(code)) return 'notFound'
|
|
916
|
+
if (/denied|forbidden|EACCES|EPERM/.test(code)) return 'denied'
|
|
917
|
+
return 'probeFailed'
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function boundedCall(call, milliseconds = 8000) {
|
|
921
|
+
let timer
|
|
922
|
+
try {
|
|
923
|
+
return await Promise.race([Promise.resolve().then(call), new Promise((_, reject) => {
|
|
924
|
+
timer = setTimeout(() => reject(new Error('Remote request timed out')), milliseconds)
|
|
925
|
+
})])
|
|
926
|
+
} finally { clearTimeout(timer) }
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function createBridge(ctx) {
|
|
930
|
+
const remote = ctx.remote
|
|
931
|
+
const sessions = ctx.sessions
|
|
932
|
+
// Both namespaces are optional so a Host without one still gets the rows it
|
|
933
|
+
// can serve: no workspaceFiles means no verification and no read actions.
|
|
934
|
+
// Cordis throws on an undeclared sub-service, so both are declared in
|
|
935
|
+
// `inject` and every access stays guarded.
|
|
936
|
+
let files = null
|
|
937
|
+
let sessionApi = null
|
|
938
|
+
try { files = remote && remote.workspaceFiles ? remote.workspaceFiles : null } catch { files = null }
|
|
939
|
+
try { sessionApi = remote && remote.session ? remote.session : null } catch { sessionApi = null }
|
|
940
|
+
|
|
941
|
+
function activeSession(sessionId) {
|
|
942
|
+
try {
|
|
943
|
+
const snapshot = sessions && sessions.list && sessions.list.getSnapshot
|
|
944
|
+
? sessions.list.getSnapshot()
|
|
945
|
+
: null
|
|
946
|
+
const rows = snapshot && snapshot.byId ? Object.values(snapshot.byId) : []
|
|
947
|
+
if (sessionId) {
|
|
948
|
+
const exact = rows.find((row) => row && row.id === sessionId)
|
|
949
|
+
// The Host can resolve a Session header even when the Client has not
|
|
950
|
+
// retained it. Leave relative paths for that Session's Remote.
|
|
951
|
+
return { id: sessionId, cwd: exact?.cwd }
|
|
952
|
+
}
|
|
953
|
+
if (rows.length === 0) return null
|
|
954
|
+
const retained = rows.find((row) => row && row.retainedBy && row.retainedBy.mainView > 0)
|
|
955
|
+
const pick = retained || rows.find((row) => row && typeof row.cwd === 'string' && row.cwd !== '')
|
|
956
|
+
return pick ? { id: pick.id, cwd: pick.cwd } : null
|
|
957
|
+
} catch {
|
|
958
|
+
return null
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return {
|
|
963
|
+
activeSession,
|
|
964
|
+
/** Resolve one path to a real file or directory, or null when it is neither. */
|
|
965
|
+
async probe(sessionId, path) {
|
|
966
|
+
if (!files) return { error: 'unavailable' }
|
|
967
|
+
let failure = 'probeFailed'
|
|
968
|
+
try {
|
|
969
|
+
const result = await boundedCall(() => files.stat(sessionId, path))
|
|
970
|
+
const stat = unwrap(result)
|
|
971
|
+
if (!result?.ok) failure = probeError(result?.error)
|
|
972
|
+
if (stat && typeof stat.absolutePath === 'string') {
|
|
973
|
+
return { isDir: false, absolutePath: stat.absolutePath, bytes: stat.bytes, version: stat.version }
|
|
974
|
+
}
|
|
975
|
+
} catch (error) { failure = probeError(error) }
|
|
976
|
+
try {
|
|
977
|
+
const listing = unwrap(await boundedCall(() => files.list(sessionId, path)))
|
|
978
|
+
if (listing && Array.isArray(listing.entries)) {
|
|
979
|
+
return { isDir: true, absolutePath: path, bytes: undefined }
|
|
980
|
+
}
|
|
981
|
+
} catch { /* not a directory either */ }
|
|
982
|
+
return { error: failure }
|
|
983
|
+
},
|
|
984
|
+
async applications(path) {
|
|
985
|
+
if (!sessionApi) return { state: 'failed', apps: [] }
|
|
986
|
+
try {
|
|
987
|
+
const result = await boundedCall(() => sessionApi.workspacePathApplications({ path }))
|
|
988
|
+
const value = unwrap(result)
|
|
989
|
+
return result?.ok && Array.isArray(value) ? { state: 'ready', apps: value } : { state: 'failed', apps: [] }
|
|
990
|
+
} catch {
|
|
991
|
+
return { state: 'failed', apps: [] }
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
async open(path, appId) {
|
|
995
|
+
if (!sessionApi) return false
|
|
996
|
+
try {
|
|
997
|
+
const request = appId === undefined ? { path } : { path, application: appId }
|
|
998
|
+
return unwrap(await sessionApi.openWorkspacePath(request)) !== null
|
|
999
|
+
} catch {
|
|
1000
|
+
return false
|
|
1001
|
+
}
|
|
1002
|
+
},
|
|
1003
|
+
async reveal(path) {
|
|
1004
|
+
if (!sessionApi) return false
|
|
1005
|
+
try {
|
|
1006
|
+
return unwrap(await sessionApi.openWorkspacePath({ path, action: 'reveal' })) !== null
|
|
1007
|
+
} catch {
|
|
1008
|
+
return false
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
async readBytes(sessionId, path, options) {
|
|
1012
|
+
if (!files) return null
|
|
1013
|
+
try {
|
|
1014
|
+
return unwrap(await files.readBytes(sessionId, path, options || {}))
|
|
1015
|
+
} catch {
|
|
1016
|
+
return null
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/* ───────────────────────────── detection ───────────────────────────── */
|
|
1023
|
+
|
|
1024
|
+
const PATH_ATTRS = ['data-file-actions-path', 'data-files-path', 'data-review-file']
|
|
1025
|
+
|
|
1026
|
+
function isEditable(node) {
|
|
1027
|
+
if (!node || !node.closest) return false
|
|
1028
|
+
return node.closest('input, textarea, [contenteditable]:not([contenteditable="false"])') !== null
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** Expand the text under the pointer to one path-like token. */
|
|
1032
|
+
function tokenAtPoint(doc, x, y) {
|
|
1033
|
+
if (!doc || typeof doc.caretRangeFromPoint !== 'function') return ''
|
|
1034
|
+
let range = null
|
|
1035
|
+
try {
|
|
1036
|
+
range = doc.caretRangeFromPoint(x, y)
|
|
1037
|
+
} catch {
|
|
1038
|
+
return ''
|
|
1039
|
+
}
|
|
1040
|
+
if (!range || !range.startContainer || range.startContainer.nodeType !== 3) return ''
|
|
1041
|
+
const text = range.startContainer.nodeValue || ''
|
|
1042
|
+
const at = Math.min(Math.max(range.startOffset, 0), text.length)
|
|
1043
|
+
const isPathChar = (ch) => /[\w.@()+\-\[\]{}~#$%&',=!\\/:]/.test(ch)
|
|
1044
|
+
let start = at
|
|
1045
|
+
let end = at
|
|
1046
|
+
while (start > 0 && isPathChar(text[start - 1])) start -= 1
|
|
1047
|
+
while (end < text.length && isPathChar(text[end])) end += 1
|
|
1048
|
+
return text.slice(start, end).replace(/^[`'"(<[]+/, '').replace(/[`'")>\],.;:!?]+$/, '')
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* A soft source (a tooltip, an element's text, the caret token) only counts when
|
|
1053
|
+
* it looks like an actual file: an absolute path or a name with an extension.
|
|
1054
|
+
* "2026/10/22 12:00", "2/3 files" and "src/app.ts · 2.1 KB" all contain a slash
|
|
1055
|
+
* but are prose, and swallowing the native menu for them is worse than missing
|
|
1056
|
+
* an exotic path.
|
|
1057
|
+
*/
|
|
1058
|
+
function softPath(value) {
|
|
1059
|
+
const text = String(value || '').trim()
|
|
1060
|
+
if (text === '') return false
|
|
1061
|
+
if (isAbsolutePath(text)) return true
|
|
1062
|
+
return /\.[A-Za-z0-9]{1,10}$/.test(text)
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* A delivery table prefixes the name with an icon glyph ("📄发布文案.md"), and a
|
|
1067
|
+
* chip may start with a bullet or arrow. Strip that decoration before the name
|
|
1068
|
+
* is tested as a path — never strip a leading dot, slash or drive letter.
|
|
1069
|
+
*/
|
|
1070
|
+
function stripDecorations(value) {
|
|
1071
|
+
return String(value || '').replace(/^[^\p{L}\p{N}._\\/-]+/u, '').trim()
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Find the file a right-click points at. Structured carriers (a title or a
|
|
1076
|
+
* data attribute the GUI already writes) win; a bare text token is the
|
|
1077
|
+
* fallback for surfaces that only render the path as text.
|
|
1078
|
+
*/
|
|
1079
|
+
function candidateFromEvent(event, doc) {
|
|
1080
|
+
const target = event.target
|
|
1081
|
+
if (!target || !target.closest) return null
|
|
1082
|
+
if (isEditable(target)) return null
|
|
1083
|
+
|
|
1084
|
+
// A resource address owns its Session and path, even when its child has a
|
|
1085
|
+
// different title. Do not reinterpret it against the selected chat.
|
|
1086
|
+
const resourceAnchor = target.closest('a[href^="dsh-resource://file/"]')
|
|
1087
|
+
if (resourceAnchor) {
|
|
1088
|
+
const href = resourceAnchor.getAttribute('href')
|
|
1089
|
+
const resourcePath = pathFromHref(href)
|
|
1090
|
+
const session = /^dsh-resource:\/\/file\/session\/([^/]+)\//i.exec(href)
|
|
1091
|
+
if (!resourcePath) return null
|
|
1092
|
+
let sessionId = session?.[1]
|
|
1093
|
+
try { if (sessionId) sessionId = decodeURIComponent(sessionId) } catch { return null }
|
|
1094
|
+
return { path: resourcePath, sessionId, source: 'href', element: resourceAnchor }
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
for (const attr of PATH_ATTRS) {
|
|
1098
|
+
const holder = target.closest(`[${attr}]`)
|
|
1099
|
+
if (holder) {
|
|
1100
|
+
const value = holder.getAttribute(attr)
|
|
1101
|
+
if (value && looksLikePath(value)) return { path: value, source: attr, element: holder }
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const card = target.closest('[data-presented-file]')
|
|
1106
|
+
if (card) {
|
|
1107
|
+
const titled = card.querySelector('[title]')
|
|
1108
|
+
const value = titled && titled.getAttribute('title')
|
|
1109
|
+
if (value && looksLikePath(value)) return { path: value, source: 'presented', element: card }
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
let node = target
|
|
1113
|
+
for (let depth = 0; depth < 5 && node && node !== doc.body; depth += 1, node = node.parentElement) {
|
|
1114
|
+
const value = node.getAttribute && node.getAttribute('title')
|
|
1115
|
+
if (value && looksLikePath(value) && softPath(value)) return { path: value, source: 'title', element: node }
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const anchor = target.closest('a[href]')
|
|
1119
|
+
if (anchor) {
|
|
1120
|
+
const href = String(anchor.getAttribute('href') || '')
|
|
1121
|
+
const fromHref = pathFromHref(href)
|
|
1122
|
+
if (fromHref) return { path: fromHref, source: 'href', element: anchor }
|
|
1123
|
+
// A blue reference can also be an ordinary web link (a GitHub release, a
|
|
1124
|
+
// discussion). It gets the same menu shape with link actions instead of
|
|
1125
|
+
// file actions, so the two kinds of blue text behave alike.
|
|
1126
|
+
const link = linkFromHref(href)
|
|
1127
|
+
if (link) return { link, source: 'href', element: anchor }
|
|
1128
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href)) return null
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// The element's own text is often exactly the path (a tool row's file link,
|
|
1132
|
+
// a chip). Cheaper and steadier than caret hit-testing. Delivery-table cells
|
|
1133
|
+
// arrive as "📄发布文案.md", so the decoration goes first.
|
|
1134
|
+
const own = stripDecorations(target.textContent)
|
|
1135
|
+
if (own && own.length <= 512 && looksLikePath(own) && softPath(own)) {
|
|
1136
|
+
return { path: own, source: 'text', element: target }
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const token = stripDecorations(tokenAtPoint(doc, event.clientX, event.clientY))
|
|
1140
|
+
if (token && looksLikePath(token) && softPath(token)) return { path: token, source: 'text', element: target }
|
|
1141
|
+
return null
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/* ───────────────────────────── menu widget ───────────────────────────── */
|
|
1145
|
+
|
|
1146
|
+
function makeNode(tag, props, children) {
|
|
1147
|
+
const node = document.createElement(tag)
|
|
1148
|
+
if (props) {
|
|
1149
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1150
|
+
if (value === undefined || value === null || value === false) continue
|
|
1151
|
+
if (key === 'html') node.innerHTML = value
|
|
1152
|
+
else if (key === 'text') node.textContent = value
|
|
1153
|
+
else if (key === 'class') node.className = value
|
|
1154
|
+
else node.setAttribute(key, String(value))
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
for (const child of children || []) {
|
|
1158
|
+
if (child === null || child === undefined) continue
|
|
1159
|
+
node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child)
|
|
1160
|
+
}
|
|
1161
|
+
return node
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
class FileMenu {
|
|
1165
|
+
constructor(options) {
|
|
1166
|
+
this.model = options.model
|
|
1167
|
+
this.x = options.x
|
|
1168
|
+
this.y = options.y
|
|
1169
|
+
this.onPick = options.onPick
|
|
1170
|
+
this.onClose = options.onClose
|
|
1171
|
+
this.keyboard = options.keyboard === true
|
|
1172
|
+
this.anchor = options.anchor || null
|
|
1173
|
+
this.t = options.t || ZH
|
|
1174
|
+
this.root = null
|
|
1175
|
+
this.body = null
|
|
1176
|
+
this.thumbUrl = null
|
|
1177
|
+
this.submenu = null
|
|
1178
|
+
this.submenuFor = null
|
|
1179
|
+
this.subItems = []
|
|
1180
|
+
this.subVisible = []
|
|
1181
|
+
this.subIndex = -1
|
|
1182
|
+
this.searchInput = null
|
|
1183
|
+
this.subBody = null
|
|
1184
|
+
this.subEmpty = null
|
|
1185
|
+
this.items = []
|
|
1186
|
+
this.index = -1
|
|
1187
|
+
this.closed = false
|
|
1188
|
+
this.handleKeyDown = this.handleKeyDown.bind(this)
|
|
1189
|
+
this.handlePointerDown = this.handlePointerDown.bind(this)
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
open() {
|
|
1193
|
+
const root = makeNode('div', {
|
|
1194
|
+
class: 'dfa-menu',
|
|
1195
|
+
role: 'menu',
|
|
1196
|
+
tabindex: '-1',
|
|
1197
|
+
[MENU_ATTR]: '1',
|
|
1198
|
+
'aria-label': this.model.title,
|
|
1199
|
+
})
|
|
1200
|
+
this.root = root
|
|
1201
|
+
this.body = makeNode('div', { class: 'dfa-body' })
|
|
1202
|
+
root.appendChild(this.body)
|
|
1203
|
+
const floor = menuFloor()
|
|
1204
|
+
if (floor) root.style.setProperty('--dfa-floor', floor)
|
|
1205
|
+
this.previousFocus = document.activeElement
|
|
1206
|
+
root.setAttribute('data-keyboard', this.keyboard ? 'true' : 'false')
|
|
1207
|
+
root.addEventListener('mousedown', (event) => { if (!event.target.closest('input,button')) event.preventDefault() })
|
|
1208
|
+
this.body.addEventListener('scroll', () => this.updateFade(), { passive: true })
|
|
1209
|
+
document.body.appendChild(root)
|
|
1210
|
+
this.render()
|
|
1211
|
+
this.place(root, this.x, this.y)
|
|
1212
|
+
document.addEventListener('keydown', this.handleKeyDown, true)
|
|
1213
|
+
document.addEventListener('pointerdown', this.handlePointerDown, true)
|
|
1214
|
+
window.addEventListener('resize', this.handlePointerDown, true)
|
|
1215
|
+
window.addEventListener('blur', this.handlePointerDown, true)
|
|
1216
|
+
// Move focus into the menu so the composer does not keep receiving the
|
|
1217
|
+
// keystrokes meant for the rows; close() hands focus back. The window
|
|
1218
|
+
// blur listener ignores descendant blurs, so this cannot close the menu.
|
|
1219
|
+
try { root.focus({ preventScroll: true }) } catch { /* older engine */ }
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/** Fade the scroll edges so a clipped list reads as scrollable. */
|
|
1223
|
+
updateFade() {
|
|
1224
|
+
const body = this.body
|
|
1225
|
+
if (!body) return
|
|
1226
|
+
const top = body.scrollTop > 2
|
|
1227
|
+
const bottom = body.scrollTop + body.clientHeight < body.scrollHeight - 2
|
|
1228
|
+
const mode = top && bottom ? 'both' : top ? 'top' : bottom ? 'bottom' : 'none'
|
|
1229
|
+
if (body.dataset.fade !== mode) body.dataset.fade = mode
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Paint the current model into the existing body. File associations arrive a
|
|
1234
|
+
* few seconds after the first right-click, so the same menu is re-rendered in
|
|
1235
|
+
* place instead of opening late or flickering through a close/reopen.
|
|
1236
|
+
*/
|
|
1237
|
+
render() {
|
|
1238
|
+
if (!this.root || !this.body) return
|
|
1239
|
+
this.closeSubmenu()
|
|
1240
|
+
const scrollTop = this.body.scrollTop
|
|
1241
|
+
const position = this.root.getBoundingClientRect()
|
|
1242
|
+
const painted = this.items.length > 0
|
|
1243
|
+
const keep = this.items[this.index] ? this.items[this.index].item.id : null
|
|
1244
|
+
this.body.textContent = ''
|
|
1245
|
+
this.items = []
|
|
1246
|
+
this.index = -1
|
|
1247
|
+
if (this.model.preview) this.body.appendChild(this.renderThumb())
|
|
1248
|
+
this.body.appendChild(this.renderHead())
|
|
1249
|
+
this.model.sections.forEach((section, at) => {
|
|
1250
|
+
if (at > 0) this.body.appendChild(makeNode('div', { class: 'dfa-sep' }))
|
|
1251
|
+
for (const item of section) this.body.appendChild(this.renderItem(item))
|
|
1252
|
+
})
|
|
1253
|
+
this.body.appendChild(makeNode('div', { class: 'dfa-foot', text: this.t.nativeHint }))
|
|
1254
|
+
if (this.keyboard) {
|
|
1255
|
+
this.body.appendChild(makeNode('div', { class: 'dfa-foot' }, [
|
|
1256
|
+
makeNode('span', { text: this.t.keysMove }),
|
|
1257
|
+
makeNode('span', { text: this.t.keysRun }),
|
|
1258
|
+
makeNode('span', { text: this.t.keysClose }),
|
|
1259
|
+
]))
|
|
1260
|
+
}
|
|
1261
|
+
const restored = keep === null ? -1 : this.items.findIndex((entry) => entry.item.id === keep)
|
|
1262
|
+
// A pointer-opened menu keeps no row highlighted; the keyboard opener and
|
|
1263
|
+
// the post-enrichment repaint both restore a real selection.
|
|
1264
|
+
if (restored >= 0 && !this.items[restored].disabled) this.focus(restored)
|
|
1265
|
+
else if (this.keyboard) this.focus(this.items.findIndex((entry) => !entry.disabled))
|
|
1266
|
+
this.body.scrollTop = scrollTop
|
|
1267
|
+
if (this.root.parentNode) {
|
|
1268
|
+
// Keep the chosen side of the pointer; only clamp when the viewport ends.
|
|
1269
|
+
this.place(this.root, painted ? position.left : this.x, painted ? position.top : this.y, painted)
|
|
1270
|
+
}
|
|
1271
|
+
this.root.setAttribute('aria-label', this.model.title)
|
|
1272
|
+
requestAnimationFrame(() => this.updateFade())
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
renderThumb() {
|
|
1276
|
+
const wrap = makeNode('div', { class: 'dfa-thumb' })
|
|
1277
|
+
if (this.thumbUrl) wrap.appendChild(makeNode('img', { src: this.thumbUrl, alt: this.model.title }))
|
|
1278
|
+
else wrap.textContent = this.thumbnailDone ? this.t.previewUnavailable : this.t.previewLoading
|
|
1279
|
+
return wrap
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/** Drop an image preview above the header once its bytes have arrived. */
|
|
1283
|
+
setThumbnail(url) {
|
|
1284
|
+
if (this.closed) return
|
|
1285
|
+
this.thumbUrl = url
|
|
1286
|
+
this.thumbnailDone = true
|
|
1287
|
+
const previous = this.body?.querySelector('.dfa-thumb')
|
|
1288
|
+
if (previous) previous.replaceWith(this.renderThumb())
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* The header is the one deliberate element: a tinted file-type tile, the name,
|
|
1293
|
+
* and the path that answers "which file is this" without leaving the menu.
|
|
1294
|
+
*/
|
|
1295
|
+
renderHead() {
|
|
1296
|
+
const model = this.model
|
|
1297
|
+
const tile = makeNode('span', { class: 'dfa-tile', 'data-kind': model.kind || 'binary' })
|
|
1298
|
+
tile.innerHTML = iconSvg(KIND_ICONS[model.kind] || 'doc')
|
|
1299
|
+
const text = makeNode('span', { class: 'dfa-headtext' }, [
|
|
1300
|
+
makeNode('span', { class: 'dfa-name', text: model.title, title: model.subtitle }),
|
|
1301
|
+
makeNode('span', { class: 'dfa-path', text: model.pathLine || model.subtitle, title: model.subtitle }),
|
|
1302
|
+
])
|
|
1303
|
+
const aside = makeNode('span', { class: 'dfa-aside' })
|
|
1304
|
+
if (model.outsideWorkspace) aside.appendChild(makeNode('span', { class: 'dfa-badge', text: this.t.outsideWorkspace }))
|
|
1305
|
+
// A reference like `file.ts:42` names a line the Host cannot jump to, so the
|
|
1306
|
+
// header states it instead of silently dropping the suffix.
|
|
1307
|
+
if (model.locationLabel) aside.appendChild(makeNode('span', { class: 'dfa-loc', text: model.locationLabel }))
|
|
1308
|
+
if (model.sizeLabel) aside.appendChild(makeNode('span', { class: 'dfa-size', text: model.sizeLabel }))
|
|
1309
|
+
return makeNode('div', { class: 'dfa-head' }, [tile, text, aside])
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** Replace the model (rows only) and repaint without moving the menu. */
|
|
1313
|
+
setModel(model) {
|
|
1314
|
+
if (this.closed) return
|
|
1315
|
+
this.model = model
|
|
1316
|
+
this.render()
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
renderItem(item) {
|
|
1320
|
+
const row = makeNode('div', {
|
|
1321
|
+
class: 'dfa-item',
|
|
1322
|
+
role: 'menuitem',
|
|
1323
|
+
tabindex: '-1',
|
|
1324
|
+
id: `dfa-row-${this.items.length}`,
|
|
1325
|
+
[ITEM_ATTR]: item.id,
|
|
1326
|
+
'aria-disabled': item.disabled ? 'true' : 'false',
|
|
1327
|
+
'aria-haspopup': item.children ? 'menu' : 'false',
|
|
1328
|
+
'aria-expanded': item.children ? 'false' : undefined,
|
|
1329
|
+
})
|
|
1330
|
+
const icon = makeNode('span', { class: 'dfa-ico' })
|
|
1331
|
+
if (item.iconData) {
|
|
1332
|
+
icon.appendChild(makeNode('img', { src: item.iconData, alt: '' }))
|
|
1333
|
+
} else {
|
|
1334
|
+
icon.innerHTML = iconSvg(item.icon)
|
|
1335
|
+
}
|
|
1336
|
+
row.appendChild(icon)
|
|
1337
|
+
row.appendChild(makeNode('span', { class: 'dfa-label', text: item.label }))
|
|
1338
|
+
if (item.children) {
|
|
1339
|
+
row.appendChild(makeNode('span', { class: 'dfa-tail', html: iconSvg('chevron') }))
|
|
1340
|
+
}
|
|
1341
|
+
const entry = { element: row, item, disabled: item.disabled === true }
|
|
1342
|
+
this.items.push(entry)
|
|
1343
|
+
row.addEventListener('mouseenter', () => {
|
|
1344
|
+
this.focus(this.items.indexOf(entry))
|
|
1345
|
+
if (item.children) this.openSubmenu(entry)
|
|
1346
|
+
else this.closeSubmenu()
|
|
1347
|
+
})
|
|
1348
|
+
row.addEventListener('click', (event) => {
|
|
1349
|
+
event.preventDefault()
|
|
1350
|
+
event.stopPropagation()
|
|
1351
|
+
if (entry.disabled) return
|
|
1352
|
+
if (item.children) {
|
|
1353
|
+
this.openSubmenu(entry)
|
|
1354
|
+
return
|
|
1355
|
+
}
|
|
1356
|
+
if (!item.keepOpen) this.close()
|
|
1357
|
+
this.onPick(item)
|
|
1358
|
+
})
|
|
1359
|
+
return row
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
openSubmenu(entry) {
|
|
1363
|
+
if (this.submenuFor === entry.element) return
|
|
1364
|
+
this.closeSubmenu()
|
|
1365
|
+
const submenu = makeNode('div', {
|
|
1366
|
+
class: 'dfa-menu dfa-sub',
|
|
1367
|
+
role: 'menu',
|
|
1368
|
+
[MENU_ATTR]: 'submenu',
|
|
1369
|
+
})
|
|
1370
|
+
const body = makeNode('div', { class: 'dfa-body' })
|
|
1371
|
+
submenu.appendChild(body)
|
|
1372
|
+
submenu.addEventListener('mousedown', (event) => { if (!event.target.closest('input,button')) event.preventDefault() })
|
|
1373
|
+
this.subItems = []
|
|
1374
|
+
this.subVisible = []
|
|
1375
|
+
this.subIndex = -1
|
|
1376
|
+
this.subBody = body
|
|
1377
|
+
// A long association list needs a header that stays while it scrolls.
|
|
1378
|
+
body.appendChild(makeNode('div', { class: 'dfa-subhead' }, [
|
|
1379
|
+
makeNode('span', { text: this.t.openWith }),
|
|
1380
|
+
makeNode('span', { text: `${entry.item.children.length} ${this.t.appsUnit}` }),
|
|
1381
|
+
]))
|
|
1382
|
+
body.appendChild(makeNode('div', { class: 'dfa-foot', text: this.t.pinHint }))
|
|
1383
|
+
const needsSearch = entry.item.children.length > 7
|
|
1384
|
+
if (needsSearch) {
|
|
1385
|
+
const wrap = makeNode('div', { class: 'dfa-searchwrap' })
|
|
1386
|
+
this.searchInput = makeNode('input', {
|
|
1387
|
+
class: 'dfa-search',
|
|
1388
|
+
type: 'text',
|
|
1389
|
+
placeholder: this.t.searchApps,
|
|
1390
|
+
'aria-label': this.t.searchApps,
|
|
1391
|
+
spellcheck: 'false',
|
|
1392
|
+
autocomplete: 'off',
|
|
1393
|
+
})
|
|
1394
|
+
this.searchInput.addEventListener('input', () => this.filterSubmenu())
|
|
1395
|
+
this.searchInput.addEventListener('keydown', (event) => event.stopPropagation())
|
|
1396
|
+
wrap.appendChild(this.searchInput)
|
|
1397
|
+
body.appendChild(wrap)
|
|
1398
|
+
}
|
|
1399
|
+
for (const item of entry.item.children) {
|
|
1400
|
+
const row = makeNode('div', {
|
|
1401
|
+
class: 'dfa-item',
|
|
1402
|
+
role: 'menuitem',
|
|
1403
|
+
tabindex: '-1',
|
|
1404
|
+
id: `dfa-subrow-${this.subItems.length}`,
|
|
1405
|
+
[ITEM_ATTR]: item.id,
|
|
1406
|
+
title: item.label,
|
|
1407
|
+
})
|
|
1408
|
+
const icon = makeNode('span', { class: 'dfa-ico' })
|
|
1409
|
+
if (item.iconData) icon.appendChild(makeNode('img', { src: item.iconData, alt: '' }))
|
|
1410
|
+
else icon.innerHTML = iconSvg('open')
|
|
1411
|
+
row.appendChild(icon)
|
|
1412
|
+
row.appendChild(makeNode('span', { class: 'dfa-label', text: item.label }))
|
|
1413
|
+
if (item.checked || item.preferred) {
|
|
1414
|
+
row.appendChild(makeNode('span', { class: 'dfa-tail' }, [
|
|
1415
|
+
makeNode('span', { class: 'dfa-default', text: item.preferred ? this.t.preferred : this.t.defaultApp }),
|
|
1416
|
+
]))
|
|
1417
|
+
}
|
|
1418
|
+
if (item.pinnable) {
|
|
1419
|
+
const pin = makeNode('button', { class: 'dfa-pin', type: 'button', tabindex: '-1',
|
|
1420
|
+
'aria-label': `${item.preferred ? this.t.unpinApp : this.t.pinApp}: ${item.label}`,
|
|
1421
|
+
'aria-pressed': item.preferred ? 'true' : 'false',
|
|
1422
|
+
title: item.preferred ? this.t.unpinApp : this.t.pinApp,
|
|
1423
|
+
text: item.preferred ? '★' : '☆' })
|
|
1424
|
+
pin.addEventListener('click', (event) => {
|
|
1425
|
+
event.preventDefault(); event.stopPropagation()
|
|
1426
|
+
this.close()
|
|
1427
|
+
this.onPick({ ...item, action: 'preferApp' })
|
|
1428
|
+
})
|
|
1429
|
+
row.appendChild(pin)
|
|
1430
|
+
}
|
|
1431
|
+
const index = this.subItems.length
|
|
1432
|
+
const record = { element: row, item, label: String(item.label).toLowerCase() }
|
|
1433
|
+
this.subItems.push(record)
|
|
1434
|
+
this.subVisible.push(record)
|
|
1435
|
+
row.addEventListener('mouseenter', () => this.focusSub(index))
|
|
1436
|
+
row.addEventListener('click', (event) => {
|
|
1437
|
+
event.preventDefault()
|
|
1438
|
+
event.stopPropagation()
|
|
1439
|
+
this.close()
|
|
1440
|
+
this.onPick(item)
|
|
1441
|
+
})
|
|
1442
|
+
body.appendChild(row)
|
|
1443
|
+
}
|
|
1444
|
+
this.subEmpty = makeNode('div', { class: 'dfa-empty', text: this.t.noMatch })
|
|
1445
|
+
this.subEmpty.hidden = true
|
|
1446
|
+
body.appendChild(this.subEmpty)
|
|
1447
|
+
const floor = menuFloor()
|
|
1448
|
+
if (floor) submenu.style.setProperty('--dfa-floor', floor)
|
|
1449
|
+
document.body.appendChild(submenu)
|
|
1450
|
+
this.placeSubmenu(submenu, entry.element)
|
|
1451
|
+
this.submenu = submenu
|
|
1452
|
+
this.submenuFor = entry.element
|
|
1453
|
+
entry.element.setAttribute('aria-expanded', 'true')
|
|
1454
|
+
body.addEventListener('scroll', () => this.updateSubFade(), { passive: true })
|
|
1455
|
+
requestAnimationFrame(() => this.updateSubFade())
|
|
1456
|
+
if (this.keyboard && this.searchInput) {
|
|
1457
|
+
try { this.searchInput.focus({ preventScroll: true }) } catch { /* older engine */ }
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/** Filter the association rows by a case-insensitive substring. */
|
|
1462
|
+
filterSubmenu() {
|
|
1463
|
+
const query = String(this.searchInput ? this.searchInput.value : '').trim().toLowerCase()
|
|
1464
|
+
this.subVisible = []
|
|
1465
|
+
for (const record of this.subItems) {
|
|
1466
|
+
const match = query === '' || record.label.includes(query)
|
|
1467
|
+
record.element.hidden = !match
|
|
1468
|
+
if (match) this.subVisible.push(record)
|
|
1469
|
+
}
|
|
1470
|
+
this.subEmpty.hidden = this.subVisible.length > 0
|
|
1471
|
+
this.subIndex = -1
|
|
1472
|
+
for (const record of this.subItems) record.element.dataset.focus = 'false'
|
|
1473
|
+
if (this.submenu) this.submenu.removeAttribute('aria-activedescendant')
|
|
1474
|
+
if (this.subVisible.length > 0) this.focusSub(0)
|
|
1475
|
+
this.updateSubFade()
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
updateSubFade() {
|
|
1479
|
+
const body = this.subBody
|
|
1480
|
+
if (!body) return
|
|
1481
|
+
const top = body.scrollTop > 2
|
|
1482
|
+
const bottom = body.scrollTop + body.clientHeight < body.scrollHeight - 2
|
|
1483
|
+
const mode = top && bottom ? 'both' : top ? 'top' : bottom ? 'bottom' : 'none'
|
|
1484
|
+
if (body.dataset.fade !== mode) body.dataset.fade = mode
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/** Roving focus inside the submenu; the parent menu keeps its own index. */
|
|
1488
|
+
focusSub(index) {
|
|
1489
|
+
const rows = this.subVisible
|
|
1490
|
+
if (rows.length === 0) return
|
|
1491
|
+
const next = Math.min(Math.max(index, 0), rows.length - 1)
|
|
1492
|
+
const target = rows[next]
|
|
1493
|
+
for (const record of this.subItems) record.element.dataset.focus = 'false'
|
|
1494
|
+
target.element.dataset.focus = 'true'
|
|
1495
|
+
// Keep the parent menu's own row highlighted while the submenu is open.
|
|
1496
|
+
this.subIndex = this.subItems.indexOf(target)
|
|
1497
|
+
if (this.submenu) this.submenu.setAttribute('aria-activedescendant', target.element.id)
|
|
1498
|
+
if (target.element.scrollIntoView) target.element.scrollIntoView({ block: 'nearest' })
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
moveSub(delta) {
|
|
1502
|
+
const rows = this.subVisible
|
|
1503
|
+
if (rows.length === 0) return
|
|
1504
|
+
const current = this.subVisible.length > 0 ? this.subVisible.indexOf(this.subItems[this.subIndex]) : this.subIndex
|
|
1505
|
+
const from = current < 0 ? (delta > 0 ? -1 : 0) : current
|
|
1506
|
+
const next = (from + delta + rows.length) % rows.length
|
|
1507
|
+
this.focusSub(next)
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
activateSub(prefer = false) {
|
|
1511
|
+
const rows = this.subVisible
|
|
1512
|
+
const current = this.subItems[this.subIndex]
|
|
1513
|
+
const entry = rows.includes(current) ? current : rows[0]
|
|
1514
|
+
if (!entry) return
|
|
1515
|
+
this.close()
|
|
1516
|
+
this.onPick(prefer && entry.item.pinnable ? { ...entry.item, action: 'preferApp' } : entry.item)
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Anchor the submenu to its row, flipping to the parent menu's left side when
|
|
1521
|
+
* the right edge would clip — flipping relative to the row would cover the
|
|
1522
|
+
* menu it belongs to.
|
|
1523
|
+
*/
|
|
1524
|
+
placeSubmenu(submenu, row) {
|
|
1525
|
+
submenu.style.left = '0px'
|
|
1526
|
+
submenu.style.top = '0px'
|
|
1527
|
+
const box = row.getBoundingClientRect()
|
|
1528
|
+
const rect = submenu.getBoundingClientRect()
|
|
1529
|
+
const parent = this.root ? this.root.getBoundingClientRect() : box
|
|
1530
|
+
const vw = window.innerWidth
|
|
1531
|
+
const vh = window.innerHeight
|
|
1532
|
+
let left = parent.right - 4
|
|
1533
|
+
if (left + rect.width > vw - 8) left = parent.left - rect.width + 4
|
|
1534
|
+
if (left < 8) left = 8
|
|
1535
|
+
let top = box.top - 4
|
|
1536
|
+
if (top + rect.height > vh - 8) top = vh - rect.height - 8
|
|
1537
|
+
if (top < 8) top = 8
|
|
1538
|
+
submenu.style.left = `${Math.round(left)}px`
|
|
1539
|
+
submenu.style.top = `${Math.round(top)}px`
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
closeSubmenu() {
|
|
1543
|
+
if (this.submenu) {
|
|
1544
|
+
this.submenuFor?.setAttribute('aria-expanded', 'false')
|
|
1545
|
+
this.submenu.remove()
|
|
1546
|
+
this.submenu = null
|
|
1547
|
+
this.submenuFor = null
|
|
1548
|
+
}
|
|
1549
|
+
this.subItems = []
|
|
1550
|
+
this.subVisible = []
|
|
1551
|
+
this.subIndex = -1
|
|
1552
|
+
this.searchInput = null
|
|
1553
|
+
this.subBody = null
|
|
1554
|
+
this.subEmpty = null
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
place(menu, x, y, preserveSide = false) {
|
|
1558
|
+
menu.style.left = '0px'
|
|
1559
|
+
menu.style.top = '0px'
|
|
1560
|
+
const rect = menu.getBoundingClientRect()
|
|
1561
|
+
const vw = window.innerWidth
|
|
1562
|
+
const vh = window.innerHeight
|
|
1563
|
+
let left = x
|
|
1564
|
+
let top = y
|
|
1565
|
+
if (left + rect.width > vw - 8) left = Math.max(8, preserveSide ? vw - rect.width - 8 : x - rect.width)
|
|
1566
|
+
if (left < 8) left = 8
|
|
1567
|
+
if (top + rect.height > vh - 8) top = Math.max(8, vh - rect.height - 8)
|
|
1568
|
+
if (top < 8) top = 8
|
|
1569
|
+
menu.style.left = `${Math.round(left)}px`
|
|
1570
|
+
menu.style.top = `${Math.round(top)}px`
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
focus(index) {
|
|
1574
|
+
if (this.items.length === 0) return
|
|
1575
|
+
const next = Math.min(Math.max(index, 0), this.items.length - 1)
|
|
1576
|
+
this.items.forEach((entry, at) => {
|
|
1577
|
+
entry.element.dataset.focus = at === next ? 'true' : 'false'
|
|
1578
|
+
})
|
|
1579
|
+
this.index = next
|
|
1580
|
+
const entry = this.items[next]
|
|
1581
|
+
if (entry && this.root) this.root.setAttribute('aria-activedescendant', entry.element.id)
|
|
1582
|
+
if (entry && entry.element.scrollIntoView) entry.element.scrollIntoView({ block: 'nearest' })
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
move(delta) {
|
|
1586
|
+
if (this.items.length === 0) return
|
|
1587
|
+
let next = this.index
|
|
1588
|
+
for (let step = 0; step < this.items.length; step += 1) {
|
|
1589
|
+
next = (next + delta + this.items.length) % this.items.length
|
|
1590
|
+
if (!this.items[next].disabled) break
|
|
1591
|
+
}
|
|
1592
|
+
this.focus(next)
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
activate(index) {
|
|
1596
|
+
const entry = this.items[index]
|
|
1597
|
+
if (!entry || entry.disabled) return
|
|
1598
|
+
if (entry.item.children) {
|
|
1599
|
+
this.openSubmenu(entry)
|
|
1600
|
+
if (this.keyboard) this.focusSub(0)
|
|
1601
|
+
return
|
|
1602
|
+
}
|
|
1603
|
+
if (!entry.item.keepOpen) this.close()
|
|
1604
|
+
this.onPick(entry.item)
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
handleKeyDown(event) {
|
|
1608
|
+
if (!this.root) return
|
|
1609
|
+
const typing = this.searchInput && event.target === this.searchInput
|
|
1610
|
+
// While the submenu is open it owns the arrows, Enter and Escape, so a
|
|
1611
|
+
// keyboard user can actually pick an application.
|
|
1612
|
+
if (this.submenu) {
|
|
1613
|
+
if (event.key === 'ArrowDown') {
|
|
1614
|
+
event.preventDefault()
|
|
1615
|
+
this.moveSub(1)
|
|
1616
|
+
return
|
|
1617
|
+
}
|
|
1618
|
+
if (event.key === 'ArrowUp') {
|
|
1619
|
+
event.preventDefault()
|
|
1620
|
+
this.moveSub(-1)
|
|
1621
|
+
return
|
|
1622
|
+
}
|
|
1623
|
+
if (event.key === 'Enter' || (event.key === ' ' && !typing)) {
|
|
1624
|
+
event.preventDefault()
|
|
1625
|
+
this.activateSub(event.shiftKey && event.key === 'Enter')
|
|
1626
|
+
return
|
|
1627
|
+
}
|
|
1628
|
+
if (event.key === 'ArrowLeft' || event.key === 'Escape') {
|
|
1629
|
+
event.preventDefault()
|
|
1630
|
+
event.stopPropagation()
|
|
1631
|
+
// Escape in a non-empty filter clears it first, then closes.
|
|
1632
|
+
if (event.key === 'Escape' && this.searchInput && this.searchInput.value !== '') {
|
|
1633
|
+
this.searchInput.value = ''
|
|
1634
|
+
this.filterSubmenu()
|
|
1635
|
+
return
|
|
1636
|
+
}
|
|
1637
|
+
this.closeSubmenu()
|
|
1638
|
+
return
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (event.key === 'Escape') {
|
|
1642
|
+
event.preventDefault()
|
|
1643
|
+
event.stopPropagation()
|
|
1644
|
+
this.close()
|
|
1645
|
+
return
|
|
1646
|
+
}
|
|
1647
|
+
if (event.key === 'ArrowDown') {
|
|
1648
|
+
event.preventDefault()
|
|
1649
|
+
this.move(1)
|
|
1650
|
+
return
|
|
1651
|
+
}
|
|
1652
|
+
if (event.key === 'ArrowUp') {
|
|
1653
|
+
event.preventDefault()
|
|
1654
|
+
this.move(-1)
|
|
1655
|
+
return
|
|
1656
|
+
}
|
|
1657
|
+
if (event.key === 'Home') {
|
|
1658
|
+
event.preventDefault()
|
|
1659
|
+
this.focus(0)
|
|
1660
|
+
return
|
|
1661
|
+
}
|
|
1662
|
+
if (event.key === 'End') {
|
|
1663
|
+
event.preventDefault()
|
|
1664
|
+
this.focus(this.items.length - 1)
|
|
1665
|
+
return
|
|
1666
|
+
}
|
|
1667
|
+
if (event.key === 'ArrowRight') {
|
|
1668
|
+
const entry = this.items[this.index]
|
|
1669
|
+
if (entry && entry.item.children) {
|
|
1670
|
+
event.preventDefault()
|
|
1671
|
+
this.openSubmenu(entry)
|
|
1672
|
+
this.focusSub(0)
|
|
1673
|
+
}
|
|
1674
|
+
return
|
|
1675
|
+
}
|
|
1676
|
+
if (event.key === 'ArrowLeft') {
|
|
1677
|
+
if (this.submenu) {
|
|
1678
|
+
event.preventDefault()
|
|
1679
|
+
this.closeSubmenu()
|
|
1680
|
+
}
|
|
1681
|
+
return
|
|
1682
|
+
}
|
|
1683
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1684
|
+
event.preventDefault()
|
|
1685
|
+
this.activate(this.index)
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
handlePointerDown(event) {
|
|
1690
|
+
if (!this.root) return
|
|
1691
|
+
// `blur` does not bubble, but a capture listener on window still sees the
|
|
1692
|
+
// blur of any descendant — including the element that loses focus to this
|
|
1693
|
+
// menu when it opens. Only a real window blur may close it.
|
|
1694
|
+
if (event.type === 'blur' && event.target !== window) return
|
|
1695
|
+
const target = event.target
|
|
1696
|
+
if (target instanceof Node && (this.root.contains(target) || (this.submenu && this.submenu.contains(target)))) return
|
|
1697
|
+
this.close()
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* The chat transcript streams, so the page scrolls under an open menu. Follow
|
|
1702
|
+
* the anchor row instead of vanishing; close only once it leaves the viewport.
|
|
1703
|
+
*/
|
|
1704
|
+
followAnchor() {
|
|
1705
|
+
if (!this.root) return
|
|
1706
|
+
const anchor = this.anchor
|
|
1707
|
+
if (!anchor || !anchor.isConnected) {
|
|
1708
|
+
this.close()
|
|
1709
|
+
return
|
|
1710
|
+
}
|
|
1711
|
+
const rect = anchor.getBoundingClientRect()
|
|
1712
|
+
if ((rect.width === 0 && rect.height === 0) || rect.bottom < 0 || rect.top > window.innerHeight) {
|
|
1713
|
+
this.close()
|
|
1714
|
+
return
|
|
1715
|
+
}
|
|
1716
|
+
this.closeSubmenu()
|
|
1717
|
+
this.x = rect.left
|
|
1718
|
+
this.y = rect.bottom
|
|
1719
|
+
this.place(this.root, this.x, this.y)
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
close() {
|
|
1723
|
+
if (this.closed) return
|
|
1724
|
+
this.closed = true
|
|
1725
|
+
this.closeSubmenu()
|
|
1726
|
+
document.removeEventListener('keydown', this.handleKeyDown, true)
|
|
1727
|
+
document.removeEventListener('pointerdown', this.handlePointerDown, true)
|
|
1728
|
+
window.removeEventListener('resize', this.handlePointerDown, true)
|
|
1729
|
+
window.removeEventListener('blur', this.handlePointerDown, true)
|
|
1730
|
+
if (this.root) this.root.remove()
|
|
1731
|
+
this.root = null
|
|
1732
|
+
this.body = null
|
|
1733
|
+
this.thumbUrl = null
|
|
1734
|
+
this.items = []
|
|
1735
|
+
const previous = this.previousFocus
|
|
1736
|
+
this.previousFocus = null
|
|
1737
|
+
if (previous && previous !== document.body && typeof previous.focus === 'function' && previous.isConnected) {
|
|
1738
|
+
try { previous.focus({ preventScroll: true }) } catch { /* the element may have unmounted */ }
|
|
1739
|
+
}
|
|
1740
|
+
if (this.onClose) this.onClose()
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/* ───────────────────────────── clipboard ───────────────────────────── */
|
|
1745
|
+
|
|
1746
|
+
async function copyText(text) {
|
|
1747
|
+
try {
|
|
1748
|
+
await navigator.clipboard.writeText(text)
|
|
1749
|
+
return true
|
|
1750
|
+
} catch { /* fall through to the legacy path */ }
|
|
1751
|
+
try {
|
|
1752
|
+
const area = document.createElement('textarea')
|
|
1753
|
+
area.value = text
|
|
1754
|
+
area.setAttribute('readonly', '')
|
|
1755
|
+
area.style.position = 'fixed'
|
|
1756
|
+
area.style.top = '-1000px'
|
|
1757
|
+
area.style.opacity = '0'
|
|
1758
|
+
document.body.appendChild(area)
|
|
1759
|
+
area.select()
|
|
1760
|
+
const ok = document.execCommand('copy')
|
|
1761
|
+
area.remove()
|
|
1762
|
+
return ok
|
|
1763
|
+
} catch {
|
|
1764
|
+
return false
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function downloadBytes(bytes, name, mime) {
|
|
1769
|
+
const blob = new Blob([bytes], { type: mime || 'application/octet-stream' })
|
|
1770
|
+
const url = URL.createObjectURL(blob)
|
|
1771
|
+
const link = document.createElement('a')
|
|
1772
|
+
link.href = url
|
|
1773
|
+
link.download = name || 'download'
|
|
1774
|
+
link.style.display = 'none'
|
|
1775
|
+
document.body.appendChild(link)
|
|
1776
|
+
link.click()
|
|
1777
|
+
link.remove()
|
|
1778
|
+
setTimeout(() => URL.revokeObjectURL(url), 60000)
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/** Hand a URL to the app's own link handling (a new window/tab). */
|
|
1782
|
+
function openExternal(url) {
|
|
1783
|
+
try {
|
|
1784
|
+
const anchor = document.createElement('a')
|
|
1785
|
+
anchor.href = url
|
|
1786
|
+
anchor.target = '_blank'
|
|
1787
|
+
anchor.rel = 'noopener noreferrer'
|
|
1788
|
+
anchor.style.display = 'none'
|
|
1789
|
+
document.body.appendChild(anchor)
|
|
1790
|
+
anchor.click()
|
|
1791
|
+
anchor.remove()
|
|
1792
|
+
return true
|
|
1793
|
+
} catch {
|
|
1794
|
+
return false
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/** Fill the window opened synchronously by the click with the file's bytes. */
|
|
1799
|
+
function fillBrowserWindow(win, bytes, mime) {
|
|
1800
|
+
try {
|
|
1801
|
+
const blob = new Blob([bytes], { type: mime })
|
|
1802
|
+
const url = URL.createObjectURL(blob)
|
|
1803
|
+
// File HTML/SVG may contain scripts. Keep working previews, but give
|
|
1804
|
+
// them an opaque origin so they cannot read the DSH page or its cookies.
|
|
1805
|
+
win.opener = null
|
|
1806
|
+
const frame = win.document.createElement('iframe')
|
|
1807
|
+
frame.setAttribute('sandbox', 'allow-scripts allow-forms allow-downloads allow-popups')
|
|
1808
|
+
frame.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;border:0'
|
|
1809
|
+
frame.src = url
|
|
1810
|
+
win.document.body.replaceChildren(frame)
|
|
1811
|
+
setTimeout(() => URL.revokeObjectURL(url), 60000)
|
|
1812
|
+
return true
|
|
1813
|
+
} catch {
|
|
1814
|
+
try { win.close() } catch { /* already gone */ }
|
|
1815
|
+
return false
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/* ─────────────────────────────── actions ─────────────────────────────── */
|
|
1820
|
+
|
|
1821
|
+
const MAX_COPY_BYTES = 2 * 1024 * 1024
|
|
1822
|
+
|
|
1823
|
+
async function dispatch(item, target, context) {
|
|
1824
|
+
const { bridge, t, sessionId } = context
|
|
1825
|
+
const path = target.absolutePath
|
|
1826
|
+
if (item.action === 'openLink' || item.action === 'copyLink' || item.action === 'copyLinkText') {
|
|
1827
|
+
if (item.action === 'openLink') {
|
|
1828
|
+
if (!openExternal(target.link)) toast(t.openFailed, { error: true })
|
|
1829
|
+
return
|
|
1830
|
+
}
|
|
1831
|
+
const value = item.action === 'copyLink' ? target.link : target.text
|
|
1832
|
+
const ok = await copyText(value || target.link)
|
|
1833
|
+
toast(ok ? (item.action === 'copyLink' ? t.copiedLink : t.copiedLinkText) : t.copyFailed, { error: !ok })
|
|
1834
|
+
return
|
|
1835
|
+
}
|
|
1836
|
+
if (item.action === 'open') {
|
|
1837
|
+
const ok = await bridge.open(path, item.appId)
|
|
1838
|
+
if (!ok) toast(t.openFailed, { error: true })
|
|
1839
|
+
return
|
|
1840
|
+
}
|
|
1841
|
+
if (item.action === 'reveal') {
|
|
1842
|
+
const ok = await bridge.reveal(path)
|
|
1843
|
+
if (!ok) toast(t.revealFailed, { error: true })
|
|
1844
|
+
return
|
|
1845
|
+
}
|
|
1846
|
+
if (item.action === 'copyPath' || item.action === 'copyRelPath' || item.action === 'copyFileName') {
|
|
1847
|
+
const value = item.action === 'copyPath' ? path
|
|
1848
|
+
: item.action === 'copyRelPath' ? target.displayPath : basename(path)
|
|
1849
|
+
const message = item.action === 'copyPath' ? t.copiedPath
|
|
1850
|
+
: item.action === 'copyRelPath' ? t.copiedRelPath : t.copiedFileName
|
|
1851
|
+
const ok = await copyText(value)
|
|
1852
|
+
toast(ok ? message : t.copyFailed, { error: !ok })
|
|
1853
|
+
return
|
|
1854
|
+
}
|
|
1855
|
+
if (item.action === 'copyReference') {
|
|
1856
|
+
const ok = await copyText(target.reference || path)
|
|
1857
|
+
toast(ok ? t.copiedReference : t.copyFailed, { error: !ok })
|
|
1858
|
+
return
|
|
1859
|
+
}
|
|
1860
|
+
if (item.action === 'copyMarkdown') {
|
|
1861
|
+
const label = String(target.text || '').replace(/([\[\]])/g, '\\$1')
|
|
1862
|
+
const ok = await copyText(`[${label || target.link}](${target.link})`)
|
|
1863
|
+
toast(ok ? t.copiedMarkdown : t.copyFailed, { error: !ok })
|
|
1864
|
+
return
|
|
1865
|
+
}
|
|
1866
|
+
if (item.action === 'copyContents' && typeof target.bytes === 'number' && target.bytes > MAX_COPY_BYTES) {
|
|
1867
|
+
// The size is already known from the stat; never pull a huge file across
|
|
1868
|
+
// the wire just to refuse it.
|
|
1869
|
+
toast(t.tooLarge, { error: true })
|
|
1870
|
+
return
|
|
1871
|
+
}
|
|
1872
|
+
if (item.action === 'browser') {
|
|
1873
|
+
// Must happen inside the click's gesture, before any await, or the popup
|
|
1874
|
+
// blocker eats it.
|
|
1875
|
+
const win = window.open('', '_blank')
|
|
1876
|
+
if (!win) {
|
|
1877
|
+
toast(t.popupBlocked, { error: true })
|
|
1878
|
+
return
|
|
1879
|
+
}
|
|
1880
|
+
win.opener = null
|
|
1881
|
+
const result = await bridge.readBytes(sessionId, path)
|
|
1882
|
+
if (!result || !(result.data instanceof Uint8Array) || result.eof === false) {
|
|
1883
|
+
try { win.close() } catch { /* already gone */ }
|
|
1884
|
+
toast(t.readFailed, { error: true })
|
|
1885
|
+
return
|
|
1886
|
+
}
|
|
1887
|
+
if (!fillBrowserWindow(win, result.data, mimeOf(path))) toast(t.popupBlocked, { error: true })
|
|
1888
|
+
return
|
|
1889
|
+
}
|
|
1890
|
+
if (item.action === 'copyContents' || item.action === 'saveAs') {
|
|
1891
|
+
// Chat text can plant absolute paths from outside the workspace; require
|
|
1892
|
+
// an explicit confirmation before its bytes reach the clipboard.
|
|
1893
|
+
if (item.action === 'copyContents' && target.outsideWorkspace === true) {
|
|
1894
|
+
const ask = context.confirm || (typeof confirm === 'function' ? confirm : null)
|
|
1895
|
+
let allowed = false
|
|
1896
|
+
try { allowed = ask ? ask(t.confirmOutsideCopy.replace('{path}', path)) : true } catch { allowed = true }
|
|
1897
|
+
if (!allowed) return
|
|
1898
|
+
}
|
|
1899
|
+
const options = item.action === 'copyContents'
|
|
1900
|
+
// The Host refuses a window above its own maxBytes (2 MB by default), so
|
|
1901
|
+
// ask for exactly the cap and read `eof === false` as "too large".
|
|
1902
|
+
? { range: { offset: 0, length: MAX_COPY_BYTES } }
|
|
1903
|
+
: {}
|
|
1904
|
+
const result = await bridge.readBytes(sessionId, path, options)
|
|
1905
|
+
if (!result || !(result.data instanceof Uint8Array) || (item.action === 'saveAs' && result.eof === false)) {
|
|
1906
|
+
toast(t.readFailed, { error: true })
|
|
1907
|
+
return
|
|
1908
|
+
}
|
|
1909
|
+
const bytes = result.data
|
|
1910
|
+
if (item.action === 'saveAs') {
|
|
1911
|
+
downloadBytes(bytes, basename(path), mimeOf(path))
|
|
1912
|
+
return
|
|
1913
|
+
}
|
|
1914
|
+
if (bytes.byteLength > MAX_COPY_BYTES || result.eof === false) {
|
|
1915
|
+
toast(t.tooLarge, { error: true })
|
|
1916
|
+
return
|
|
1917
|
+
}
|
|
1918
|
+
let text = ''
|
|
1919
|
+
try {
|
|
1920
|
+
text = new TextDecoder('utf-8', { fatal: false }).decode(bytes)
|
|
1921
|
+
} catch {
|
|
1922
|
+
toast(t.readFailed, { error: true })
|
|
1923
|
+
return
|
|
1924
|
+
}
|
|
1925
|
+
if (text.includes('\u0000')) {
|
|
1926
|
+
toast(t.binary, { error: true })
|
|
1927
|
+
return
|
|
1928
|
+
}
|
|
1929
|
+
const ok = await copyText(text)
|
|
1930
|
+
toast(ok ? t.copiedContents : t.copyFailed, { error: !ok })
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
/* ─────────────────────────────── plugin ─────────────────────────────── */
|
|
1935
|
+
|
|
1936
|
+
function detectLanguage(ctx) {
|
|
1937
|
+
try {
|
|
1938
|
+
const service = ctx.locale
|
|
1939
|
+
const candidates = [
|
|
1940
|
+
service && service.language && service.language.getSnapshot ? service.language.getSnapshot() : null,
|
|
1941
|
+
service && service.locale && service.locale.getSnapshot ? service.locale.getSnapshot() : null,
|
|
1942
|
+
service && service.current && service.current.getSnapshot ? service.current.getSnapshot() : null,
|
|
1943
|
+
]
|
|
1944
|
+
for (const value of candidates) {
|
|
1945
|
+
if (typeof value === 'string' && value) return value
|
|
1946
|
+
if (value && typeof value.language === 'string') return value.language
|
|
1947
|
+
}
|
|
1948
|
+
} catch { /* fall back to the browser */ }
|
|
1949
|
+
try {
|
|
1950
|
+
return navigator.language || 'zh-CN'
|
|
1951
|
+
} catch {
|
|
1952
|
+
return 'zh-CN'
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
function start(ctx) {
|
|
1957
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') return
|
|
1958
|
+
ensureCss()
|
|
1959
|
+
const bridge = createBridge(ctx)
|
|
1960
|
+
const t = messages(detectLanguage(ctx))
|
|
1961
|
+
|
|
1962
|
+
let menu = null
|
|
1963
|
+
let generation = 0
|
|
1964
|
+
let disposed = false
|
|
1965
|
+
|
|
1966
|
+
function closeMenu() {
|
|
1967
|
+
generation++
|
|
1968
|
+
if (menu) {
|
|
1969
|
+
menu.close()
|
|
1970
|
+
menu = null
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// Guessed text must be verified before cancelling the browser's menu. Warm
|
|
1975
|
+
// it on hover/focus; a cold right-click keeps the native menu for that click.
|
|
1976
|
+
const probeCache = new Map()
|
|
1977
|
+
const probePending = new Map()
|
|
1978
|
+
function locate(candidate) {
|
|
1979
|
+
const session = bridge.activeSession(candidate.sessionId)
|
|
1980
|
+
const located = stripLocationSuffix(candidate.path)
|
|
1981
|
+
const path = isAbsolutePath(located.path) ? located.path : joinPath(session?.cwd, located.path)
|
|
1982
|
+
return { session, located, path, key: JSON.stringify([session?.id, path]) }
|
|
1983
|
+
}
|
|
1984
|
+
function cachedProbe(candidate) {
|
|
1985
|
+
const cached = probeCache.get(locate(candidate).key)
|
|
1986
|
+
return cached && Date.now() - cached.at < 5000 ? cached.value : null
|
|
1987
|
+
}
|
|
1988
|
+
function warmCandidate(candidate) {
|
|
1989
|
+
if (!candidate || candidate.link || !['text', 'title'].includes(candidate.source)) return
|
|
1990
|
+
const { session, path, key } = locate(candidate)
|
|
1991
|
+
if (!session || !path || cachedProbe(candidate) || probePending.has(key) || probePending.size >= 4) return
|
|
1992
|
+
const pending = bridge.probe(session.id, path).then((value) => {
|
|
1993
|
+
if (disposed) return
|
|
1994
|
+
if (probeCache.size >= 64) probeCache.delete(probeCache.keys().next().value)
|
|
1995
|
+
probeCache.set(key, { value, at: Date.now() })
|
|
1996
|
+
}).finally(() => probePending.delete(key))
|
|
1997
|
+
probePending.set(key, pending)
|
|
1998
|
+
}
|
|
1999
|
+
let preferenceStorage
|
|
2000
|
+
try { preferenceStorage = window.localStorage } catch { /* private storage may be disabled */ }
|
|
2001
|
+
const preferences = applicationPreferences(preferenceStorage)
|
|
2002
|
+
|
|
2003
|
+
async function openAt(candidate, x, y, keyboard) {
|
|
2004
|
+
if (disposed) return
|
|
2005
|
+
const attempt = ++generation
|
|
2006
|
+
let handle
|
|
2007
|
+
try {
|
|
2008
|
+
if (candidate.link) {
|
|
2009
|
+
const text = String(candidate.element?.textContent || '').trim().slice(0, 512)
|
|
2010
|
+
const target = { link: candidate.link, text }
|
|
2011
|
+
handle = new FileMenu({ model: buildLinkMenuModel({ url: candidate.link, text, t }),
|
|
2012
|
+
x, y, keyboard, anchor: candidate.element, t,
|
|
2013
|
+
onClose: () => { if (menu === handle) menu = null },
|
|
2014
|
+
onPick: (item) => { void dispatch(item, target, { bridge, t }) },
|
|
2015
|
+
})
|
|
2016
|
+
menu = handle
|
|
2017
|
+
handle.open()
|
|
2018
|
+
return
|
|
2019
|
+
}
|
|
2020
|
+
const { session, located, path } = locate(candidate)
|
|
2021
|
+
const modelInput = { absolutePath: path, displayPath: located.path, apps: [], appsState: 'loading',
|
|
2022
|
+
location: located, reference: candidate.path, t }
|
|
2023
|
+
let target
|
|
2024
|
+
handle = new FileMenu({ model: buildStatusModel(path, t.checking, t),
|
|
2025
|
+
x, y, keyboard, anchor: candidate.element, t,
|
|
2026
|
+
onClose: () => { if (menu === handle) menu = null },
|
|
2027
|
+
onPick: (item) => {
|
|
2028
|
+
if (item.action === 'retry') { void openAt(candidate, x, y, keyboard); return }
|
|
2029
|
+
if (item.action === 'retryApps') {
|
|
2030
|
+
modelInput.appsState = 'loading'
|
|
2031
|
+
handle.setModel(buildMenuModel(modelInput))
|
|
2032
|
+
void enrich(handle, modelInput)
|
|
2033
|
+
return
|
|
2034
|
+
}
|
|
2035
|
+
if (item.action === 'preferApp') {
|
|
2036
|
+
const appId = preferences.get(path) === item.appId ? '' : item.appId
|
|
2037
|
+
const saved = preferences.set(path, appId)
|
|
2038
|
+
toast(saved ? (appId ? t.preferenceSaved : t.preferenceCleared) : t.preferenceFailed, { error: !saved })
|
|
2039
|
+
return
|
|
2040
|
+
}
|
|
2041
|
+
if (target) void dispatch(item, target, { bridge, t, sessionId: session.id })
|
|
2042
|
+
},
|
|
2043
|
+
})
|
|
2044
|
+
menu = handle
|
|
2045
|
+
handle.open()
|
|
2046
|
+
if (!session) {
|
|
2047
|
+
handle.setModel(buildStatusModel(path, t.noSession, t, true))
|
|
2048
|
+
return
|
|
2049
|
+
}
|
|
2050
|
+
const probe = cachedProbe(candidate) || await bridge.probe(session.id, path)
|
|
2051
|
+
if (disposed || attempt !== generation || handle.closed) return
|
|
2052
|
+
if (!probe || probe.error) {
|
|
2053
|
+
handle.setModel(buildStatusModel(path, t[probe?.error] || t.probeFailed, t, true))
|
|
2054
|
+
return
|
|
2055
|
+
}
|
|
2056
|
+
Object.assign(modelInput, { absolutePath: probe.absolutePath, isDir: probe.isDir,
|
|
2057
|
+
bytes: probe.bytes, outsideWorkspace: !isInside(session.cwd, probe.absolutePath),
|
|
2058
|
+
cwd: session.cwd, preferredId: preferences.get(probe.absolutePath) })
|
|
2059
|
+
target = { absolutePath: probe.absolutePath, displayPath: located.path,
|
|
2060
|
+
isDir: probe.isDir, line: located.line, column: located.column, bytes: probe.bytes,
|
|
2061
|
+
reference: candidate.path, outsideWorkspace: !isInside(session.cwd, probe.absolutePath) }
|
|
2062
|
+
handle.setModel(buildMenuModel(modelInput))
|
|
2063
|
+
if (!probe.isDir) void enrich(handle, modelInput)
|
|
2064
|
+
if (!probe.isDir && (classify(probe.absolutePath, false).kind === 'image' || extname(probe.absolutePath) === 'svg')) {
|
|
2065
|
+
void loadThumb(handle, session.id, probe.absolutePath, probe.bytes, probe.version)
|
|
2066
|
+
}
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
if (!disposed && attempt === generation && handle && !handle.closed) {
|
|
2069
|
+
handle.setModel(buildStatusModel(candidate.path, t.probeFailed, t, true))
|
|
2070
|
+
}
|
|
2071
|
+
console.warn('[dsh-file-actions] context menu failed:', error)
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Associations are per file type, so one query per extension serves the page.
|
|
2076
|
+
// Ten minutes keeps a newly installed editor visible without re-asking the
|
|
2077
|
+
// OS on every right-click.
|
|
2078
|
+
const APPS_TTL_MS = 10 * 60 * 1000
|
|
2079
|
+
const appsCache = new Map()
|
|
2080
|
+
async function enrich(handle, input) {
|
|
2081
|
+
const key = extname(input.absolutePath).toLowerCase()
|
|
2082
|
+
const cached = appsCache.get(key)
|
|
2083
|
+
const result = cached && Date.now() - cached.at < cached.ttl
|
|
2084
|
+
? cached.result : await bridge.applications(input.absolutePath)
|
|
2085
|
+
if (disposed) return
|
|
2086
|
+
// Successful empty answers are different from a failed Remote query.
|
|
2087
|
+
if (result.state === 'ready') appsCache.set(key, { result, at: Date.now(), ttl: result.apps.length ? APPS_TTL_MS : 15000 })
|
|
2088
|
+
if (menu !== handle || handle.closed) return
|
|
2089
|
+
Object.assign(input, { apps: result.apps, appsState: result.state })
|
|
2090
|
+
handle.setModel(buildMenuModel(input))
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// A thumbnail is worth one bounded read; bigger files just keep the tile.
|
|
2094
|
+
const THUMB_MAX_BYTES = 8 * 1024 * 1024
|
|
2095
|
+
const thumbCache = new Map()
|
|
2096
|
+
async function loadThumb(handle, sessionId, absolutePath, bytes, version) {
|
|
2097
|
+
try {
|
|
2098
|
+
if (typeof bytes === 'number' && bytes > THUMB_MAX_BYTES) { handle.setThumbnail(null); return }
|
|
2099
|
+
const key = JSON.stringify([sessionId, absolutePath, version])
|
|
2100
|
+
const cached = version && thumbCache.get(key)
|
|
2101
|
+
if (cached) {
|
|
2102
|
+
if (menu === handle && !handle.closed) handle.setThumbnail(cached.url)
|
|
2103
|
+
return
|
|
2104
|
+
}
|
|
2105
|
+
const result = await bridge.readBytes(sessionId, absolutePath)
|
|
2106
|
+
if (disposed || menu !== handle || handle.closed) return
|
|
2107
|
+
if (!result || !(result.data instanceof Uint8Array) || result.eof === false || result.data.byteLength > THUMB_MAX_BYTES) {
|
|
2108
|
+
handle.setThumbnail(null)
|
|
2109
|
+
return
|
|
2110
|
+
}
|
|
2111
|
+
const url = URL.createObjectURL(new Blob([result.data], { type: mimeOf(absolutePath) }))
|
|
2112
|
+
const previous = thumbCache.get(key)
|
|
2113
|
+
if (previous) URL.revokeObjectURL(previous.url)
|
|
2114
|
+
thumbCache.set(key, { url, at: Date.now() })
|
|
2115
|
+
if (thumbCache.size > 12) {
|
|
2116
|
+
const oldest = [...thumbCache.entries()].sort((a, b) => a[1].at - b[1].at)[0]
|
|
2117
|
+
if (oldest) {
|
|
2118
|
+
URL.revokeObjectURL(oldest[1].url)
|
|
2119
|
+
thumbCache.delete(oldest[0])
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
if (menu === handle && !handle.closed) handle.setThumbnail(url)
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
if (menu === handle && !handle.closed) handle.setThumbnail(null)
|
|
2125
|
+
console.warn('[dsh-file-actions] thumbnail failed:', error)
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
function onContextMenu(event) {
|
|
2130
|
+
if (event.defaultPrevented) return
|
|
2131
|
+
if (event.shiftKey) { closeMenu(); return }
|
|
2132
|
+
const target = event.target
|
|
2133
|
+
// A right-click on the menu itself must not open a second menu over it.
|
|
2134
|
+
if (target && target.closest && target.closest(`[${MENU_ATTR}]`)) {
|
|
2135
|
+
event.preventDefault()
|
|
2136
|
+
event.stopPropagation()
|
|
2137
|
+
return
|
|
2138
|
+
}
|
|
2139
|
+
closeMenu()
|
|
2140
|
+
const candidate = candidateFromEvent(event, document)
|
|
2141
|
+
if (!candidate) return
|
|
2142
|
+
if (['text', 'title'].includes(candidate.source)) {
|
|
2143
|
+
const probe = cachedProbe(candidate)
|
|
2144
|
+
if (!probe || probe.error) { warmCandidate(candidate); return }
|
|
2145
|
+
}
|
|
2146
|
+
event.preventDefault()
|
|
2147
|
+
event.stopPropagation()
|
|
2148
|
+
void openAt(candidate, event.clientX, event.clientY, false)
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
function onKeyDown(event) {
|
|
2152
|
+
const wantsMenu = (event.key === 'F10' && event.shiftKey) || event.key === 'ContextMenu'
|
|
2153
|
+
if (!wantsMenu) return
|
|
2154
|
+
const active = document.activeElement
|
|
2155
|
+
if (!active || !active.getBoundingClientRect) return
|
|
2156
|
+
const rect = active.getBoundingClientRect()
|
|
2157
|
+
const candidate = candidateFromEvent({
|
|
2158
|
+
target: active,
|
|
2159
|
+
clientX: rect.left + rect.width / 2,
|
|
2160
|
+
clientY: rect.top + rect.height / 2,
|
|
2161
|
+
}, document)
|
|
2162
|
+
if (!candidate) return
|
|
2163
|
+
if (['text', 'title'].includes(candidate.source)) {
|
|
2164
|
+
const probe = cachedProbe(candidate)
|
|
2165
|
+
if (!probe || probe.error) { warmCandidate(candidate); return }
|
|
2166
|
+
}
|
|
2167
|
+
event.preventDefault()
|
|
2168
|
+
void openAt(candidate, rect.left, rect.bottom, true)
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
function onWarm(event) {
|
|
2172
|
+
if (event.shiftKey || event.target?.closest?.(`[${MENU_ATTR}]`)) return
|
|
2173
|
+
const rect = event.target?.getBoundingClientRect?.()
|
|
2174
|
+
warmCandidate(candidateFromEvent({ target: event.target,
|
|
2175
|
+
clientX: event.clientX ?? rect?.left, clientY: event.clientY ?? rect?.top }, document))
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
function onPointerDown(event) {
|
|
2179
|
+
const target = event.target
|
|
2180
|
+
if (target && target.closest && target.closest(`[${MENU_ATTR}]`)) return
|
|
2181
|
+
closeMenu()
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
function onViewportChange(event) {
|
|
2185
|
+
// Scrolling the menu's own long lists (or the "open with" submenu) is not
|
|
2186
|
+
// the page moving; closing on it would make those lists unusable.
|
|
2187
|
+
const target = event && event.target
|
|
2188
|
+
if (target && target.closest && target.closest(`[${MENU_ATTR}]`)) return
|
|
2189
|
+
if (!menu) return
|
|
2190
|
+
if (event && event.type === 'scroll') {
|
|
2191
|
+
menu.followAnchor()
|
|
2192
|
+
return
|
|
2193
|
+
}
|
|
2194
|
+
closeMenu()
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
const install = () => {
|
|
2198
|
+
if (activeCleanup) activeCleanup()
|
|
2199
|
+
document.addEventListener('contextmenu', onContextMenu, true)
|
|
2200
|
+
document.addEventListener('pointerover', onWarm, true)
|
|
2201
|
+
document.addEventListener('focusin', onWarm, true)
|
|
2202
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
2203
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
2204
|
+
window.addEventListener('resize', onViewportChange)
|
|
2205
|
+
document.addEventListener('scroll', onViewportChange, true)
|
|
2206
|
+
// A DOM marker survives page-side identity quirks and gives the e2e driver
|
|
2207
|
+
// one stable readiness signal instead of guessing at boot timing.
|
|
2208
|
+
document.documentElement.setAttribute('data-dfa-ready', '1')
|
|
2209
|
+
const cleanup = () => {
|
|
2210
|
+
disposed = true
|
|
2211
|
+
document.documentElement.removeAttribute('data-dfa-ready')
|
|
2212
|
+
document.removeEventListener('contextmenu', onContextMenu, true)
|
|
2213
|
+
document.removeEventListener('pointerover', onWarm, true)
|
|
2214
|
+
document.removeEventListener('focusin', onWarm, true)
|
|
2215
|
+
probeCache.clear()
|
|
2216
|
+
probePending.clear()
|
|
2217
|
+
document.removeEventListener('keydown', onKeyDown, true)
|
|
2218
|
+
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
2219
|
+
window.removeEventListener('resize', onViewportChange)
|
|
2220
|
+
document.removeEventListener('scroll', onViewportChange, true)
|
|
2221
|
+
closeMenu()
|
|
2222
|
+
for (const value of thumbCache.values()) URL.revokeObjectURL(value.url)
|
|
2223
|
+
thumbCache.clear()
|
|
2224
|
+
appsCache.clear()
|
|
2225
|
+
if (activeCleanup === cleanup) activeCleanup = null
|
|
2226
|
+
}
|
|
2227
|
+
activeCleanup = cleanup
|
|
2228
|
+
return cleanup
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
if (typeof ctx.effect === 'function') {
|
|
2232
|
+
ctx.effect(install, 'dsh-file-actions: context menu')
|
|
2233
|
+
} else {
|
|
2234
|
+
install()
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
/**
|
|
2239
|
+
* The loader records one failed row as a boot-audit failure with no message, so
|
|
2240
|
+
* log the real stack before rethrowing.
|
|
2241
|
+
*/
|
|
2242
|
+
function apply(ctx) {
|
|
2243
|
+
try {
|
|
2244
|
+
start(ctx)
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
console.error('[dsh-file-actions] activation failed:', (error && error.stack) || error)
|
|
2247
|
+
throw error
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
module.exports = {
|
|
2252
|
+
name: 'dsh-file-actions',
|
|
2253
|
+
inject: ['sessions', 'remote', 'remote.session', 'remote.workspaceFiles'],
|
|
2254
|
+
apply,
|
|
2255
|
+
// Exposed for the Node unit tests: the bundle is a classic script, so the
|
|
2256
|
+
// test harness evaluates it with a fake `window` and reads these directly.
|
|
2257
|
+
__internals: {
|
|
2258
|
+
messages,
|
|
2259
|
+
isAbsolutePath,
|
|
2260
|
+
normalizeSlashes,
|
|
2261
|
+
basename,
|
|
2262
|
+
extname,
|
|
2263
|
+
formatBytes,
|
|
2264
|
+
stripLocationSuffix,
|
|
2265
|
+
joinPath,
|
|
2266
|
+
looksLikePath,
|
|
2267
|
+
pathFromHref,
|
|
2268
|
+
classify,
|
|
2269
|
+
isTextName,
|
|
2270
|
+
isInside,
|
|
2271
|
+
relativeTo,
|
|
2272
|
+
softPath,
|
|
2273
|
+
stripDecorations,
|
|
2274
|
+
dispatch,
|
|
2275
|
+
mimeOf,
|
|
2276
|
+
pickEditor,
|
|
2277
|
+
orderApplications,
|
|
2278
|
+
applicationPreferences,
|
|
2279
|
+
buildStatusModel,
|
|
2280
|
+
FileMenu,
|
|
2281
|
+
buildMenuModel,
|
|
2282
|
+
buildLinkMenuModel,
|
|
2283
|
+
formatLocation,
|
|
2284
|
+
linkFromHref,
|
|
2285
|
+
candidateFromEvent,
|
|
2286
|
+
createBridge,
|
|
2287
|
+
fillBrowserWindow,
|
|
2288
|
+
tokenAtPoint,
|
|
2289
|
+
unwrap,
|
|
2290
|
+
menuFloor,
|
|
2291
|
+
MAX_COPY_BYTES,
|
|
2292
|
+
},
|
|
2293
|
+
}
|
|
2294
|
+
return module.exports
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
2298
|
+
window.__ModuleLoader__.load({ id: '@jidekaixin2dian/dsh-file-actions', factory })
|
|
2299
|
+
}
|
|
2300
|
+
})()
|