pinokiod 3.231.0 → 3.232.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kernel/bin/cuda.js +83 -76
- package/kernel/bin/setup.js +2 -0
- package/kernel/prototype.js +13 -0
- package/kernel/router/index.js +41 -0
- package/package.json +1 -1
- package/server/index.js +92 -31
- package/server/public/container-tab-link.js +115 -0
- package/server/public/style.css +4 -0
- package/server/public/tab-link-popover.css +118 -0
- package/server/public/tab-link-popover.js +1391 -0
- package/server/views/app.ejs +79 -1626
- package/server/views/container.ejs +3 -0
- package/server/views/index.ejs +6 -0
- package/server/views/net.ejs +449 -8
- package/server/views/network.ejs +5 -3
- package/server/views/partials/dynamic.ejs +1 -1
- package/server/views/partials/menu.ejs +1 -1
- package/server/views/partials/running.ejs +1 -1
package/server/views/app.ejs
CHANGED
|
@@ -860,124 +860,6 @@ body.dark .grid-btns .btn2 {
|
|
|
860
860
|
flex: 1 1 auto;
|
|
861
861
|
min-width: 0;
|
|
862
862
|
}
|
|
863
|
-
.tab-link-popover {
|
|
864
|
-
position: fixed;
|
|
865
|
-
display: none;
|
|
866
|
-
flex-direction: column;
|
|
867
|
-
gap: 4px;
|
|
868
|
-
padding: 8px 0;
|
|
869
|
-
border-radius: 10px;
|
|
870
|
-
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
871
|
-
background: rgba(255, 255, 255, 0.97);
|
|
872
|
-
box-shadow: 0 12px 32px -8px rgba(15, 23, 42, 0.25);
|
|
873
|
-
z-index: 9999;
|
|
874
|
-
min-width: 240px;
|
|
875
|
-
max-width: 420px;
|
|
876
|
-
backdrop-filter: blur(6px);
|
|
877
|
-
}
|
|
878
|
-
body.dark .tab-link-popover {
|
|
879
|
-
border-color: rgba(255, 255, 255, 0.08);
|
|
880
|
-
background: rgba(17, 24, 39, 0.95);
|
|
881
|
-
box-shadow: 0 12px 36px -12px rgba(15, 23, 42, 0.55);
|
|
882
|
-
}
|
|
883
|
-
.tab-link-popover.visible {
|
|
884
|
-
display: flex;
|
|
885
|
-
}
|
|
886
|
-
.tab-link-popover .tab-link-popover-header {
|
|
887
|
-
display: flex;
|
|
888
|
-
align-items: center;
|
|
889
|
-
gap: 8px;
|
|
890
|
-
padding: 10px 14px 6px;
|
|
891
|
-
font-size: 11px;
|
|
892
|
-
font-weight: 600;
|
|
893
|
-
letter-spacing: 0.05em;
|
|
894
|
-
text-transform: uppercase;
|
|
895
|
-
color: rgba(15, 23, 42, 0.55);
|
|
896
|
-
}
|
|
897
|
-
.tab-link-popover .tab-link-popover-header i {
|
|
898
|
-
font-size: 12px;
|
|
899
|
-
}
|
|
900
|
-
body.dark .tab-link-popover .tab-link-popover-header {
|
|
901
|
-
color: rgba(226, 232, 240, 0.7);
|
|
902
|
-
}
|
|
903
|
-
.tab-link-popover .tab-link-popover-item {
|
|
904
|
-
width: 100%;
|
|
905
|
-
border: none;
|
|
906
|
-
margin: 0;
|
|
907
|
-
padding: 8px 14px;
|
|
908
|
-
display: flex;
|
|
909
|
-
flex-direction: column;
|
|
910
|
-
gap: 2px;
|
|
911
|
-
text-align: left;
|
|
912
|
-
font: inherit;
|
|
913
|
-
color: inherit;
|
|
914
|
-
background: transparent;
|
|
915
|
-
cursor: pointer;
|
|
916
|
-
}
|
|
917
|
-
.tab-link-popover .tab-link-popover-item.qr-inline { flex-direction: row; align-items: center; gap: 10px; }
|
|
918
|
-
.tab-link-popover .tab-link-popover-item.qr-inline .textcol { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
|
|
919
|
-
.tab-link-popover .tab-link-popover-item.qr-inline .qr { width: 64px; height: 64px; image-rendering: pixelated; flex: 0 0 auto; margin-left: auto; }
|
|
920
|
-
.tab-link-popover .tab-link-popover-item:hover,
|
|
921
|
-
.tab-link-popover .tab-link-popover-item:focus-visible {
|
|
922
|
-
background: rgba(15, 23, 42, 0.06);
|
|
923
|
-
outline: none;
|
|
924
|
-
}
|
|
925
|
-
body.dark .tab-link-popover .tab-link-popover-item:hover,
|
|
926
|
-
body.dark .tab-link-popover .tab-link-popover-item:focus-visible {
|
|
927
|
-
background: rgba(148, 163, 184, 0.12);
|
|
928
|
-
}
|
|
929
|
-
.tab-link-popover .tab-link-popover-item .label {
|
|
930
|
-
font-size: 11px;
|
|
931
|
-
font-weight: 600;
|
|
932
|
-
letter-spacing: 0.04em;
|
|
933
|
-
text-transform: uppercase;
|
|
934
|
-
color: rgba(15, 23, 42, 0.55);
|
|
935
|
-
}
|
|
936
|
-
body.dark .tab-link-popover .tab-link-popover-item .label {
|
|
937
|
-
color: rgba(226, 232, 240, 0.65);
|
|
938
|
-
}
|
|
939
|
-
.tab-link-popover .tab-link-popover-item .value {
|
|
940
|
-
font-size: 13px;
|
|
941
|
-
line-height: 1.35;
|
|
942
|
-
word-break: break-word;
|
|
943
|
-
color: rgba(15, 23, 42, 0.85);
|
|
944
|
-
}
|
|
945
|
-
body.dark .tab-link-popover .tab-link-popover-item .value {
|
|
946
|
-
color: rgba(226, 232, 240, 0.9);
|
|
947
|
-
}
|
|
948
|
-
.tab-link-popover .tab-link-popover-item .value .muted {
|
|
949
|
-
color: rgba(15, 23, 42, 0.55);
|
|
950
|
-
}
|
|
951
|
-
body.dark .tab-link-popover .tab-link-popover-item .value .muted {
|
|
952
|
-
color: rgba(226, 232, 240, 0.65);
|
|
953
|
-
}
|
|
954
|
-
.tab-link-popover .tab-link-popover-footer {
|
|
955
|
-
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
|
956
|
-
margin-top: 4px;
|
|
957
|
-
padding-top: 12px;
|
|
958
|
-
background: rgba(59, 130, 246, 0.12);
|
|
959
|
-
color: #1d4ed8;
|
|
960
|
-
}
|
|
961
|
-
.tab-link-popover .tab-link-popover-footer .label,
|
|
962
|
-
.tab-link-popover .tab-link-popover-footer .value {
|
|
963
|
-
color: inherit;
|
|
964
|
-
}
|
|
965
|
-
.tab-link-popover .tab-link-popover-footer .value {
|
|
966
|
-
font-weight: 600;
|
|
967
|
-
}
|
|
968
|
-
.tab-link-popover .tab-link-popover-footer:hover,
|
|
969
|
-
.tab-link-popover .tab-link-popover-footer:focus-visible {
|
|
970
|
-
background: rgba(37, 99, 235, 0.2);
|
|
971
|
-
}
|
|
972
|
-
body.dark .tab-link-popover .tab-link-popover-footer {
|
|
973
|
-
border-top-color: rgba(148, 163, 184, 0.2);
|
|
974
|
-
background: rgba(96, 165, 250, 0.22);
|
|
975
|
-
color: #bfdbfe;
|
|
976
|
-
}
|
|
977
|
-
body.dark .tab-link-popover .tab-link-popover-footer:hover,
|
|
978
|
-
body.dark .tab-link-popover .tab-link-popover-footer:focus-visible {
|
|
979
|
-
background: rgba(147, 197, 253, 0.35);
|
|
980
|
-
}
|
|
981
863
|
.tab.has-preview .tab-preview {
|
|
982
864
|
color: rgba(0, 0, 0, 0.6);
|
|
983
865
|
min-width: 0;
|
|
@@ -3058,6 +2940,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3058
2940
|
*/
|
|
3059
2941
|
</style>
|
|
3060
2942
|
<link href="/app.css" rel="stylesheet"/>
|
|
2943
|
+
<link href="/tab-link-popover.css" rel="stylesheet"/>
|
|
3061
2944
|
<script src="/window_storage.js"></script>
|
|
3062
2945
|
<script src="/timeago.min.js"></script>
|
|
3063
2946
|
<script src="/hotkeys.min.js"></script>
|
|
@@ -3081,6 +2964,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3081
2964
|
<script src="/fseditor.js"></script>
|
|
3082
2965
|
<script src="/popper.min.js"></script>
|
|
3083
2966
|
<script src="/tippy-bundle.umd.min.js"></script>
|
|
2967
|
+
<script src="/tab-link-popover.js"></script>
|
|
3084
2968
|
</head>
|
|
3085
2969
|
<body class='<%=theme%>' data-platform="<%=platform%>" data-agent="<%=agent%>">
|
|
3086
2970
|
<header class='navheader grabbable'>
|
|
@@ -3113,7 +2997,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3113
2997
|
<div><i class="fa-solid fa-plus"></i></div>
|
|
3114
2998
|
</button>
|
|
3115
2999
|
<button class='btn2 hidden' id='close-window' data-tippy-content='close this section'>
|
|
3116
|
-
<div><i class="fa-solid fa-
|
|
3000
|
+
<div><i class="fa-solid fa-stop"></i></div>
|
|
3117
3001
|
</button>
|
|
3118
3002
|
</h1>
|
|
3119
3003
|
</header>
|
|
@@ -3128,23 +3012,6 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3128
3012
|
<% } else { %>
|
|
3129
3013
|
<% if (type !== 'files') { %>
|
|
3130
3014
|
<aside class='active'>
|
|
3131
|
-
<!--
|
|
3132
|
-
<div class='header-top header-item'>
|
|
3133
|
-
<% if (type === "run") { %>
|
|
3134
|
-
<div class='app-info'>
|
|
3135
|
-
<div class='app-info-card'>
|
|
3136
|
-
<% if (config.icon) { %>
|
|
3137
|
-
<img src="<%=config.icon%>" onerror="this.src='/pinokio-black.png'"/>
|
|
3138
|
-
<% } %>
|
|
3139
|
-
<div class='app-info-container'>
|
|
3140
|
-
<div class='app-info-title'><%=config.title%></div>
|
|
3141
|
-
<div class='app-info-description collapsed'><%=config.description%></div>
|
|
3142
|
-
</div>
|
|
3143
|
-
</div>
|
|
3144
|
-
</div>
|
|
3145
|
-
<% } %>
|
|
3146
|
-
</div>
|
|
3147
|
-
-->
|
|
3148
3015
|
<div class='menu-container'>
|
|
3149
3016
|
<div class='m n system' data-type="n">
|
|
3150
3017
|
<%if (type==='browse') { %>
|
|
@@ -3155,9 +3022,6 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3155
3022
|
<% } %>
|
|
3156
3023
|
<div><%=config.title%></div>
|
|
3157
3024
|
</div>
|
|
3158
|
-
<!--
|
|
3159
|
-
<div class='loader'><i class='fa-solid fa-angle-right'></i></div>
|
|
3160
|
-
-->
|
|
3161
3025
|
</a>
|
|
3162
3026
|
|
|
3163
3027
|
<div class="dynamic <%=type==='run' ? '' : 'selected'%>">
|
|
@@ -3428,1426 +3292,98 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3428
3292
|
const updatedClass = "tab-updated"
|
|
3429
3293
|
const tabStateStore = new Map()
|
|
3430
3294
|
const TAB_STATE_STORAGE_KEY = "tab-state-store"
|
|
3431
|
-
const
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
let tabLinkPendingLink = null
|
|
3435
|
-
let tabLinkHideTimer = null
|
|
3436
|
-
let tabLinkLocalInfoPromise = null
|
|
3437
|
-
let tabLinkLocalInfoExpiry = 0
|
|
3438
|
-
let tabLinkRouterInfoPromise = null
|
|
3439
|
-
let tabLinkRouterInfoExpiry = 0
|
|
3440
|
-
let tabLinkRouterHttpsActive = null
|
|
3441
|
-
let tabLinkPeerInfoPromise = null
|
|
3442
|
-
let tabLinkPeerInfoExpiry = 0
|
|
3443
|
-
|
|
3444
|
-
const ensureTabLinkPopoverEl = () => {
|
|
3445
|
-
if (!tabLinkPopoverEl) {
|
|
3446
|
-
tabLinkPopoverEl = document.createElement("div")
|
|
3447
|
-
tabLinkPopoverEl.id = TAB_LINK_POPOVER_ID
|
|
3448
|
-
tabLinkPopoverEl.className = "tab-link-popover"
|
|
3449
|
-
tabLinkPopoverEl.addEventListener("mouseenter", () => {
|
|
3450
|
-
if (tabLinkHideTimer) {
|
|
3451
|
-
clearTimeout(tabLinkHideTimer)
|
|
3452
|
-
tabLinkHideTimer = null
|
|
3453
|
-
}
|
|
3454
|
-
})
|
|
3455
|
-
tabLinkPopoverEl.addEventListener("mouseleave", () => {
|
|
3456
|
-
hideTabLinkPopover({ immediate: true })
|
|
3457
|
-
})
|
|
3458
|
-
tabLinkPopoverEl.addEventListener("click", (event) => {
|
|
3459
|
-
const item = event.target.closest(".tab-link-popover-item")
|
|
3460
|
-
if (!item) {
|
|
3461
|
-
return
|
|
3462
|
-
}
|
|
3463
|
-
event.preventDefault()
|
|
3464
|
-
event.stopPropagation()
|
|
3465
|
-
const url = item.getAttribute("data-url")
|
|
3466
|
-
if (url) {
|
|
3467
|
-
const targetMode = (item.getAttribute("data-target") || "_blank").toLowerCase()
|
|
3468
|
-
if (targetMode === "_self") {
|
|
3469
|
-
window.location.assign(url)
|
|
3470
|
-
} else {
|
|
3471
|
-
window.open(url, "_blank", "noopener")
|
|
3472
|
-
}
|
|
3473
|
-
}
|
|
3474
|
-
hideTabLinkPopover({ immediate: true })
|
|
3475
|
-
})
|
|
3476
|
-
document.body.appendChild(tabLinkPopoverEl)
|
|
3477
|
-
}
|
|
3478
|
-
return tabLinkPopoverEl
|
|
3479
|
-
}
|
|
3480
|
-
|
|
3481
|
-
const ensurePeerInfo = async () => {
|
|
3482
|
-
const now = Date.now()
|
|
3483
|
-
if (!tabLinkPeerInfoPromise || now > tabLinkPeerInfoExpiry) {
|
|
3484
|
-
tabLinkPeerInfoPromise = fetch("/pinokio/peer", {
|
|
3485
|
-
method: "GET",
|
|
3486
|
-
headers: {
|
|
3487
|
-
"Accept": "application/json"
|
|
3488
|
-
}
|
|
3489
|
-
})
|
|
3490
|
-
.then((response) => {
|
|
3491
|
-
if (!response.ok) {
|
|
3492
|
-
throw new Error("Failed to load peer info")
|
|
3493
|
-
}
|
|
3494
|
-
return response.json()
|
|
3495
|
-
})
|
|
3496
|
-
.catch(() => null)
|
|
3497
|
-
tabLinkPeerInfoExpiry = now + 3000
|
|
3498
|
-
}
|
|
3499
|
-
return tabLinkPeerInfoPromise
|
|
3500
|
-
}
|
|
3501
|
-
|
|
3502
|
-
const canonicalizeUrl = (value) => {
|
|
3503
|
-
try {
|
|
3504
|
-
const parsed = new URL(value, location.origin)
|
|
3505
|
-
if (!parsed.protocol) {
|
|
3506
|
-
return value
|
|
3507
|
-
}
|
|
3508
|
-
const protocol = parsed.protocol.toLowerCase()
|
|
3509
|
-
if (protocol !== "http:" && protocol !== "https:") {
|
|
3510
|
-
return value
|
|
3511
|
-
}
|
|
3512
|
-
const hostname = parsed.hostname.toLowerCase()
|
|
3513
|
-
const port = parsed.port ? `:${parsed.port}` : ""
|
|
3514
|
-
let pathname = parsed.pathname || "/"
|
|
3515
|
-
if (pathname !== "/") {
|
|
3516
|
-
pathname = pathname.replace(/\/+/g, "/")
|
|
3517
|
-
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
3518
|
-
pathname = pathname.slice(0, -1)
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3521
|
-
const search = parsed.search || ""
|
|
3522
|
-
return `${protocol}//${hostname}${port}${pathname}${search}`
|
|
3523
|
-
} catch (_) {
|
|
3524
|
-
return value
|
|
3525
|
-
}
|
|
3526
|
-
}
|
|
3527
|
-
|
|
3528
|
-
const ensureHttpDirectoryUrl = (value) => {
|
|
3529
|
-
try {
|
|
3530
|
-
const parsed = new URL(value)
|
|
3531
|
-
if (parsed.protocol.toLowerCase() !== "http:") {
|
|
3532
|
-
return value
|
|
3533
|
-
}
|
|
3534
|
-
let pathname = parsed.pathname || "/"
|
|
3535
|
-
const lastSegment = pathname.split("/").pop() || ""
|
|
3536
|
-
const hasExtension = lastSegment.includes(".")
|
|
3537
|
-
if (!hasExtension && !pathname.endsWith("/")) {
|
|
3538
|
-
pathname = `${pathname}/`
|
|
3539
|
-
parsed.pathname = pathname
|
|
3540
|
-
}
|
|
3541
|
-
parsed.hash = parsed.hash || ""
|
|
3542
|
-
parsed.search = parsed.search || ""
|
|
3543
|
-
return parsed.toString()
|
|
3544
|
-
} catch (_) {
|
|
3545
|
-
return value
|
|
3546
|
-
}
|
|
3547
|
-
}
|
|
3548
|
-
|
|
3549
|
-
const isLocalHostLike = (hostname) => {
|
|
3550
|
-
if (!hostname) {
|
|
3551
|
-
return false
|
|
3552
|
-
}
|
|
3553
|
-
const hostLower = hostname.toLowerCase()
|
|
3554
|
-
if (hostLower === location.hostname.toLowerCase()) {
|
|
3555
|
-
return true
|
|
3556
|
-
}
|
|
3557
|
-
if (hostLower === "localhost" || hostLower === "0.0.0.0") {
|
|
3558
|
-
return true
|
|
3559
|
-
}
|
|
3560
|
-
if (hostLower.startsWith("127.")) {
|
|
3561
|
-
return true
|
|
3562
|
-
}
|
|
3563
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostLower)) {
|
|
3564
|
-
return true
|
|
3295
|
+
const getWindowStorage = () => {
|
|
3296
|
+
if (typeof windowStorage === "undefined" || !windowStorage) {
|
|
3297
|
+
return null
|
|
3565
3298
|
}
|
|
3566
|
-
return
|
|
3299
|
+
return windowStorage
|
|
3567
3300
|
}
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
const extractProjectSlug = (node) => {
|
|
3572
|
-
if (!node) {
|
|
3301
|
+
const frameContextKey = () => {
|
|
3302
|
+
const frameName = window.frameElement?.name || ""
|
|
3303
|
+
if (!frameName) {
|
|
3573
3304
|
return ""
|
|
3574
3305
|
}
|
|
3575
|
-
const
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
candidates.push(targetFull)
|
|
3579
|
-
}
|
|
3580
|
-
const dataHref = node.getAttribute("href")
|
|
3581
|
-
if (typeof dataHref === "string" && dataHref.length > 0) {
|
|
3582
|
-
candidates.push(dataHref)
|
|
3306
|
+
const datasetSrc = window.frameElement?.dataset?.src
|
|
3307
|
+
if (typeof datasetSrc === 'string' && datasetSrc.length > 0) {
|
|
3308
|
+
return `${frameName}:${datasetSrc}`
|
|
3583
3309
|
}
|
|
3584
3310
|
try {
|
|
3585
|
-
const
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
// ignore
|
|
3589
|
-
}
|
|
3590
|
-
for (const candidate of candidates) {
|
|
3591
|
-
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
3592
|
-
continue
|
|
3593
|
-
}
|
|
3594
|
-
const assetMatch = candidate.match(/\/asset\/api\/([^\/?#]+)/i)
|
|
3595
|
-
if (assetMatch && assetMatch[1]) {
|
|
3596
|
-
return assetMatch[1]
|
|
3597
|
-
}
|
|
3598
|
-
const pageMatch = candidate.match(/\/p\/([^\/?#]+)/i)
|
|
3599
|
-
if (pageMatch && pageMatch[1]) {
|
|
3600
|
-
return pageMatch[1]
|
|
3311
|
+
const srcUrl = window.frameElement?.src ? new URL(window.frameElement.src, window.location.origin) : null
|
|
3312
|
+
if (srcUrl) {
|
|
3313
|
+
return `${frameName}:${srcUrl.pathname}${srcUrl.search}${srcUrl.hash}`
|
|
3601
3314
|
}
|
|
3602
|
-
}
|
|
3603
|
-
return
|
|
3604
|
-
}
|
|
3605
|
-
|
|
3606
|
-
const formatDisplayUrl = (value) => {
|
|
3607
|
-
try {
|
|
3608
|
-
const parsed = new URL(value, location.origin)
|
|
3609
|
-
const host = parsed.host
|
|
3610
|
-
const pathname = parsed.pathname || "/"
|
|
3611
|
-
const search = parsed.search || ""
|
|
3612
|
-
return `${host}${pathname}${search}`
|
|
3613
|
-
} catch (_) {
|
|
3614
|
-
return value
|
|
3615
|
-
}
|
|
3616
|
-
}
|
|
3617
|
-
|
|
3618
|
-
const isHttpOrHttps = (value) => {
|
|
3619
|
-
try {
|
|
3620
|
-
const parsed = new URL(value, location.origin)
|
|
3621
|
-
const protocol = parsed.protocol.toLowerCase()
|
|
3622
|
-
return protocol === "http:" || protocol === "https:"
|
|
3623
|
-
} catch (_) {
|
|
3624
|
-
return false
|
|
3625
|
-
}
|
|
3315
|
+
} catch (_) {}
|
|
3316
|
+
return frameName
|
|
3626
3317
|
}
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
return parsed.protocol.toLowerCase() === "http:"
|
|
3632
|
-
} catch (_) {
|
|
3633
|
-
return false
|
|
3318
|
+
const selectionStorageKey = () => {
|
|
3319
|
+
const base = frameContextKey()
|
|
3320
|
+
if (!base) {
|
|
3321
|
+
return ""
|
|
3634
3322
|
}
|
|
3323
|
+
return `${base}:selector`
|
|
3635
3324
|
}
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
return parsed.protocol.toLowerCase() === "https:"
|
|
3641
|
-
} catch (_) {
|
|
3642
|
-
return false
|
|
3325
|
+
const selectionUrlStorageKey = () => {
|
|
3326
|
+
const base = frameContextKey()
|
|
3327
|
+
if (!base) {
|
|
3328
|
+
return ""
|
|
3643
3329
|
}
|
|
3330
|
+
return `${base}:url`
|
|
3644
3331
|
}
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
continue
|
|
3660
|
-
}
|
|
3661
|
-
visited.add(current)
|
|
3662
|
-
const values = Array.isArray(current) ? current : Object.values(current)
|
|
3663
|
-
for (const value of values) {
|
|
3664
|
-
if (typeof value === "string") {
|
|
3665
|
-
if (isHttpOrHttps(value)) {
|
|
3666
|
-
urls.add(value)
|
|
3667
|
-
}
|
|
3668
|
-
} else if (value && typeof value === "object") {
|
|
3669
|
-
queue.push(value)
|
|
3670
|
-
}
|
|
3671
|
-
}
|
|
3332
|
+
const SELECTION_SELECTOR_ATTRS = [
|
|
3333
|
+
'target',
|
|
3334
|
+
'data-target-full',
|
|
3335
|
+
'href',
|
|
3336
|
+
'data-shell',
|
|
3337
|
+
'data-script',
|
|
3338
|
+
'data-action',
|
|
3339
|
+
'data-run',
|
|
3340
|
+
'data-command',
|
|
3341
|
+
'data-filepath'
|
|
3342
|
+
]
|
|
3343
|
+
const buildFrameLinkSelector = (node) => {
|
|
3344
|
+
if (!node || !node.classList || !node.classList.contains('frame-link')) {
|
|
3345
|
+
return null
|
|
3672
3346
|
}
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
const keys = new Set()
|
|
3678
|
-
const scriptAttr = node.getAttribute("data-script")
|
|
3679
|
-
if (scriptAttr) {
|
|
3680
|
-
const decoded = decodeURIComponent(scriptAttr)
|
|
3681
|
-
if (decoded) {
|
|
3682
|
-
keys.add(decoded)
|
|
3683
|
-
const withoutQuery = decoded.split("?")[0]
|
|
3684
|
-
if (withoutQuery) {
|
|
3685
|
-
keys.add(withoutQuery)
|
|
3686
|
-
}
|
|
3347
|
+
for (const attr of SELECTION_SELECTOR_ATTRS) {
|
|
3348
|
+
const raw = node.getAttribute(attr)
|
|
3349
|
+
if (typeof raw === 'string' && raw.length > 0) {
|
|
3350
|
+
return `.frame-link[${attr}='${escapeTargetSelector(raw)}']`
|
|
3687
3351
|
}
|
|
3688
3352
|
}
|
|
3689
|
-
|
|
3690
|
-
if (filepathAttr) {
|
|
3691
|
-
keys.add(filepathAttr)
|
|
3692
|
-
}
|
|
3693
|
-
return Array.from(keys)
|
|
3694
|
-
}
|
|
3695
|
-
|
|
3696
|
-
const ensureLocalMemory = async () => {
|
|
3697
|
-
const now = Date.now()
|
|
3698
|
-
if (!tabLinkLocalInfoPromise || now > tabLinkLocalInfoExpiry) {
|
|
3699
|
-
tabLinkLocalInfoPromise = fetch("/info/local", {
|
|
3700
|
-
method: "GET",
|
|
3701
|
-
headers: {
|
|
3702
|
-
"Accept": "application/json"
|
|
3703
|
-
}
|
|
3704
|
-
})
|
|
3705
|
-
.then((response) => {
|
|
3706
|
-
if (!response.ok) {
|
|
3707
|
-
throw new Error("Failed to load local info")
|
|
3708
|
-
}
|
|
3709
|
-
return response.json()
|
|
3710
|
-
})
|
|
3711
|
-
.catch(() => ({}))
|
|
3712
|
-
tabLinkLocalInfoExpiry = now + 3000
|
|
3713
|
-
}
|
|
3714
|
-
return tabLinkLocalInfoPromise
|
|
3353
|
+
return null
|
|
3715
3354
|
}
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
return ""
|
|
3720
|
-
}
|
|
3721
|
-
let trimmed = value.trim()
|
|
3722
|
-
if (!trimmed) {
|
|
3723
|
-
return ""
|
|
3355
|
+
const findLinkByAbsoluteHref = (href) => {
|
|
3356
|
+
if (!href) {
|
|
3357
|
+
return null
|
|
3724
3358
|
}
|
|
3725
|
-
|
|
3726
|
-
if (/^https?:\/\//i.test(trimmed)) {
|
|
3359
|
+
return Array.from(document.querySelectorAll('.frame-link')).find((el) => {
|
|
3727
3360
|
try {
|
|
3728
|
-
|
|
3729
|
-
const host = (parsed.hostname || '').toLowerCase()
|
|
3730
|
-
if (!host || isIPv4Host(host)) {
|
|
3731
|
-
return ""
|
|
3732
|
-
}
|
|
3733
|
-
// Only accept domains (prefer *.localhost) for HTTPS targets
|
|
3734
|
-
if (!(host === 'localhost' || host.endsWith('.localhost') || host.includes('.'))) {
|
|
3735
|
-
return ""
|
|
3736
|
-
}
|
|
3737
|
-
let pathname = parsed.pathname || ""
|
|
3738
|
-
if (pathname === "/") pathname = ""
|
|
3739
|
-
const search = parsed.search || ""
|
|
3740
|
-
return `https://${host}${pathname}${search}`
|
|
3361
|
+
return el.href === href
|
|
3741
3362
|
} catch (_) {
|
|
3742
|
-
return
|
|
3743
|
-
}
|
|
3744
|
-
}
|
|
3745
|
-
// Not a full URL: accept plain domains (prefer *.localhost), reject IPs
|
|
3746
|
-
try {
|
|
3747
|
-
const hostCandidate = trimmed.split('/')[0].toLowerCase()
|
|
3748
|
-
if (!hostCandidate || isIPv4Host(hostCandidate)) {
|
|
3749
|
-
return ""
|
|
3750
|
-
}
|
|
3751
|
-
if (!(hostCandidate === 'localhost' || hostCandidate.endsWith('.localhost') || hostCandidate.includes('.'))) {
|
|
3752
|
-
return ""
|
|
3363
|
+
return false
|
|
3753
3364
|
}
|
|
3754
|
-
|
|
3755
|
-
} catch (_) {
|
|
3756
|
-
return ""
|
|
3757
|
-
}
|
|
3365
|
+
}) || null
|
|
3758
3366
|
}
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
if (!
|
|
3762
|
-
return
|
|
3763
|
-
}
|
|
3764
|
-
let trimmed = value.trim()
|
|
3765
|
-
if (!trimmed) {
|
|
3766
|
-
return null
|
|
3367
|
+
const persistFrameLinkSelection = (node) => {
|
|
3368
|
+
const storage = getWindowStorage()
|
|
3369
|
+
if (!storage || !node || !node.classList || !node.classList.contains('frame-link')) {
|
|
3370
|
+
return
|
|
3767
3371
|
}
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3372
|
+
const payload = {
|
|
3373
|
+
selector: buildFrameLinkSelector(node),
|
|
3374
|
+
hrefAttr: node.getAttribute('href') || null,
|
|
3375
|
+
href: (() => {
|
|
3376
|
+
try {
|
|
3377
|
+
return node.href || null
|
|
3378
|
+
} catch (_) {
|
|
3772
3379
|
return null
|
|
3773
3380
|
}
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
port = "443"
|
|
3781
|
-
}
|
|
3782
|
-
}
|
|
3783
|
-
if (!port) {
|
|
3784
|
-
return null
|
|
3785
|
-
}
|
|
3786
|
-
return {
|
|
3787
|
-
host: parsed.hostname.toLowerCase(),
|
|
3788
|
-
port
|
|
3789
|
-
}
|
|
3790
|
-
} catch (_) {
|
|
3791
|
-
return null
|
|
3792
|
-
}
|
|
3793
|
-
}
|
|
3794
|
-
const slashIndex = trimmed.indexOf("/")
|
|
3795
|
-
if (slashIndex >= 0) {
|
|
3796
|
-
trimmed = trimmed.slice(0, slashIndex)
|
|
3797
|
-
}
|
|
3798
|
-
const match = trimmed.match(/^\[?([^\]]+)\]?(?::([0-9]+))$/)
|
|
3799
|
-
if (!match) {
|
|
3800
|
-
return null
|
|
3801
|
-
}
|
|
3802
|
-
const host = match[1] ? match[1].toLowerCase() : ""
|
|
3803
|
-
const port = match[2] || ""
|
|
3804
|
-
if (!host || !port) {
|
|
3805
|
-
return null
|
|
3806
|
-
}
|
|
3807
|
-
return { host, port }
|
|
3808
|
-
}
|
|
3809
|
-
|
|
3810
|
-
const ensureRouterInfoMapping = async () => {
|
|
3811
|
-
const now = Date.now()
|
|
3812
|
-
if (!tabLinkRouterInfoPromise || now > tabLinkRouterInfoExpiry) {
|
|
3813
|
-
// Use lightweight router mapping to avoid favicon/installed overhead
|
|
3814
|
-
tabLinkRouterInfoPromise = fetch("/info/router", {
|
|
3815
|
-
method: "GET",
|
|
3816
|
-
headers: {
|
|
3817
|
-
"Accept": "application/json"
|
|
3818
|
-
}
|
|
3819
|
-
})
|
|
3820
|
-
.then((response) => {
|
|
3821
|
-
if (!response.ok) {
|
|
3822
|
-
throw new Error("Failed to load system info")
|
|
3823
|
-
}
|
|
3824
|
-
return response.json()
|
|
3825
|
-
})
|
|
3826
|
-
.then((data) => {
|
|
3827
|
-
if (typeof data?.https_active === "boolean") {
|
|
3828
|
-
tabLinkRouterHttpsActive = data.https_active
|
|
3829
|
-
}
|
|
3830
|
-
const processes = Array.isArray(data?.router_info) ? data.router_info : []
|
|
3831
|
-
const rewriteMapping = data?.rewrite_mapping && typeof data.rewrite_mapping === "object"
|
|
3832
|
-
? Object.values(data.rewrite_mapping)
|
|
3833
|
-
: []
|
|
3834
|
-
const portMap = new Map()
|
|
3835
|
-
const hostPortMap = new Map()
|
|
3836
|
-
const externalHttpByExtPort = new Map() // ext port -> Set of host:port (external_ip)
|
|
3837
|
-
const externalHttpByIntPort = new Map() // internal port -> Set of host:port (external_ip)
|
|
3838
|
-
const hostAliasPortMap = new Map()
|
|
3839
|
-
if (data?.router && typeof data.router === "object") {
|
|
3840
|
-
Object.entries(data.router).forEach(([dial, hosts]) => {
|
|
3841
|
-
const parsedDial = parseHostPort(dial)
|
|
3842
|
-
if (!parsedDial || !parsedDial.port) {
|
|
3843
|
-
return
|
|
3844
|
-
}
|
|
3845
|
-
if (!Array.isArray(hosts)) {
|
|
3846
|
-
return
|
|
3847
|
-
}
|
|
3848
|
-
hosts.forEach((host) => {
|
|
3849
|
-
if (typeof host !== "string") {
|
|
3850
|
-
return
|
|
3851
|
-
}
|
|
3852
|
-
const trimmed = host.trim().toLowerCase()
|
|
3853
|
-
if (!trimmed) {
|
|
3854
|
-
return
|
|
3855
|
-
}
|
|
3856
|
-
if (!hostAliasPortMap.has(trimmed)) {
|
|
3857
|
-
hostAliasPortMap.set(trimmed, new Set())
|
|
3858
|
-
}
|
|
3859
|
-
hostAliasPortMap.get(trimmed).add(parsedDial.port)
|
|
3860
|
-
})
|
|
3861
|
-
})
|
|
3862
|
-
}
|
|
3863
|
-
const localAliases = ["127.0.0.1", "localhost", "0.0.0.0", "::1", "[::1]"]
|
|
3864
|
-
|
|
3865
|
-
const addHttpMapping = (host, port, httpsSet) => {
|
|
3866
|
-
if (!host || !port || !httpsSet || httpsSet.size === 0) {
|
|
3867
|
-
return
|
|
3868
|
-
}
|
|
3869
|
-
const hostLower = host.toLowerCase()
|
|
3870
|
-
const keys = new Set([`${hostLower}:${port}`])
|
|
3871
|
-
if (localAliases.includes(hostLower)) {
|
|
3872
|
-
localAliases.forEach((alias) => keys.add(`${alias}:${port}`))
|
|
3873
|
-
}
|
|
3874
|
-
keys.forEach((key) => {
|
|
3875
|
-
if (!hostPortMap.has(key)) {
|
|
3876
|
-
hostPortMap.set(key, new Set())
|
|
3877
|
-
}
|
|
3878
|
-
const set = hostPortMap.get(key)
|
|
3879
|
-
httpsSet.forEach((url) => set.add(url))
|
|
3880
|
-
})
|
|
3881
|
-
if (localAliases.includes(hostLower)) {
|
|
3882
|
-
if (!portMap.has(port)) {
|
|
3883
|
-
portMap.set(port, new Set())
|
|
3884
|
-
}
|
|
3885
|
-
const portSet = portMap.get(port)
|
|
3886
|
-
httpsSet.forEach((url) => portSet.add(url))
|
|
3887
|
-
}
|
|
3888
|
-
}
|
|
3889
|
-
|
|
3890
|
-
const gatherHttpsTargets = (value) => {
|
|
3891
|
-
const targets = new Set()
|
|
3892
|
-
const visit = (input) => {
|
|
3893
|
-
if (!input) {
|
|
3894
|
-
return
|
|
3895
|
-
}
|
|
3896
|
-
if (Array.isArray(input)) {
|
|
3897
|
-
input.forEach(visit)
|
|
3898
|
-
return
|
|
3899
|
-
}
|
|
3900
|
-
if (typeof input === "object") {
|
|
3901
|
-
Object.values(input).forEach(visit)
|
|
3902
|
-
return
|
|
3903
|
-
}
|
|
3904
|
-
if (typeof input !== "string") {
|
|
3905
|
-
return
|
|
3906
|
-
}
|
|
3907
|
-
const normalized = normalizeHttpsTarget(input)
|
|
3908
|
-
if (normalized) {
|
|
3909
|
-
targets.add(normalized)
|
|
3910
|
-
}
|
|
3911
|
-
}
|
|
3912
|
-
visit(value)
|
|
3913
|
-
return targets
|
|
3914
|
-
}
|
|
3915
|
-
|
|
3916
|
-
const collectHostPort = (value, hostPortCandidates, portCandidates) => {
|
|
3917
|
-
if (!value) {
|
|
3918
|
-
return
|
|
3919
|
-
}
|
|
3920
|
-
if (Array.isArray(value)) {
|
|
3921
|
-
value.forEach((item) => collectHostPort(item, hostPortCandidates, portCandidates))
|
|
3922
|
-
return
|
|
3923
|
-
}
|
|
3924
|
-
if (typeof value === "object") {
|
|
3925
|
-
Object.values(value).forEach((item) => {
|
|
3926
|
-
collectHostPort(item, hostPortCandidates, portCandidates)
|
|
3927
|
-
})
|
|
3928
|
-
return
|
|
3929
|
-
}
|
|
3930
|
-
if (typeof value !== "string") {
|
|
3931
|
-
return
|
|
3932
|
-
}
|
|
3933
|
-
const parsed = parseHostPort(value)
|
|
3934
|
-
let hostLower
|
|
3935
|
-
if (parsed && parsed.host && parsed.port) {
|
|
3936
|
-
hostLower = parsed.host.toLowerCase()
|
|
3937
|
-
hostPortCandidates.add(`${hostLower}:${parsed.port}`)
|
|
3938
|
-
if (localAliases.includes(hostLower)) {
|
|
3939
|
-
portCandidates.add(parsed.port)
|
|
3940
|
-
}
|
|
3941
|
-
}
|
|
3942
|
-
const rawHost = value.replace(/^https?:\/\//i, "").split("/")[0].toLowerCase()
|
|
3943
|
-
const aliasPorts = hostAliasPortMap.get(rawHost)
|
|
3944
|
-
if (aliasPorts && aliasPorts.size > 0) {
|
|
3945
|
-
aliasPorts.forEach((aliasPort) => {
|
|
3946
|
-
hostPortCandidates.add(`${rawHost}:${aliasPort}`)
|
|
3947
|
-
if (localAliases.includes(rawHost)) {
|
|
3948
|
-
portCandidates.add(aliasPort)
|
|
3949
|
-
}
|
|
3950
|
-
})
|
|
3951
|
-
}
|
|
3952
|
-
}
|
|
3953
|
-
|
|
3954
|
-
const collectPort = (value, portCandidates) => {
|
|
3955
|
-
if (value === null || value === undefined || value === "") {
|
|
3956
|
-
return
|
|
3957
|
-
}
|
|
3958
|
-
if (Array.isArray(value)) {
|
|
3959
|
-
value.forEach((item) => collectPort(item, portCandidates))
|
|
3960
|
-
return
|
|
3961
|
-
}
|
|
3962
|
-
const port = `${value}`.trim()
|
|
3963
|
-
if (port && /^[0-9]+$/.test(port)) {
|
|
3964
|
-
portCandidates.add(port)
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
|
|
3968
|
-
const registerEntry = (entry) => {
|
|
3969
|
-
if (!entry || typeof entry !== "object") {
|
|
3970
|
-
return
|
|
3971
|
-
}
|
|
3972
|
-
const httpsTargets = new Set()
|
|
3973
|
-
const mergeTargets = (targetValue) => {
|
|
3974
|
-
const targets = gatherHttpsTargets(targetValue)
|
|
3975
|
-
targets.forEach((url) => httpsTargets.add(url))
|
|
3976
|
-
}
|
|
3977
|
-
|
|
3978
|
-
mergeTargets(entry.external_router)
|
|
3979
|
-
mergeTargets(entry.external_domain)
|
|
3980
|
-
mergeTargets(entry.https_href)
|
|
3981
|
-
mergeTargets(entry.app_href)
|
|
3982
|
-
// Some rewrite mapping entries expose domain candidates under `hosts`
|
|
3983
|
-
mergeTargets(entry.hosts)
|
|
3984
|
-
// Internal router can also include domain aliases (e.g., comfyui.localhost)
|
|
3985
|
-
mergeTargets(entry.internal_router)
|
|
3986
|
-
|
|
3987
|
-
// Record external http host:port candidates by external and internal ports for later
|
|
3988
|
-
if (entry.external_ip && typeof entry.external_ip === 'string') {
|
|
3989
|
-
const parsed = parseHostPort(entry.external_ip)
|
|
3990
|
-
if (parsed && parsed.port) {
|
|
3991
|
-
const keyExt = parsed.port
|
|
3992
|
-
if (!externalHttpByExtPort.has(keyExt)) {
|
|
3993
|
-
externalHttpByExtPort.set(keyExt, new Set())
|
|
3994
|
-
}
|
|
3995
|
-
externalHttpByExtPort.get(keyExt).add(`${parsed.host}:${parsed.port}`)
|
|
3996
|
-
const keyInt = String(entry.internal_port || '')
|
|
3997
|
-
if (keyInt) {
|
|
3998
|
-
if (!externalHttpByIntPort.has(keyInt)) {
|
|
3999
|
-
externalHttpByIntPort.set(keyInt, new Set())
|
|
4000
|
-
}
|
|
4001
|
-
externalHttpByIntPort.get(keyInt).add(`${parsed.host}:${parsed.port}`)
|
|
4002
|
-
}
|
|
4003
|
-
}
|
|
4004
|
-
}
|
|
4005
|
-
|
|
4006
|
-
if (httpsTargets.size === 0) {
|
|
4007
|
-
return
|
|
4008
|
-
}
|
|
4009
|
-
|
|
4010
|
-
const hostPortCandidates = new Set()
|
|
4011
|
-
const portCandidates = new Set()
|
|
4012
|
-
|
|
4013
|
-
collectHostPort(entry.external_ip, hostPortCandidates, portCandidates)
|
|
4014
|
-
collectHostPort(entry.internal_ip, hostPortCandidates, portCandidates)
|
|
4015
|
-
collectHostPort(entry.ip, hostPortCandidates, portCandidates)
|
|
4016
|
-
collectHostPort(entry.dial, hostPortCandidates, portCandidates)
|
|
4017
|
-
collectHostPort(entry.match, hostPortCandidates, portCandidates)
|
|
4018
|
-
collectHostPort(entry.target, hostPortCandidates, portCandidates)
|
|
4019
|
-
collectHostPort(entry.forward, hostPortCandidates, portCandidates)
|
|
4020
|
-
collectHostPort(entry.internal_router, hostPortCandidates, portCandidates)
|
|
4021
|
-
collectHostPort(entry.external_router, hostPortCandidates, portCandidates)
|
|
4022
|
-
|
|
4023
|
-
collectPort(entry.port, portCandidates)
|
|
4024
|
-
collectPort(entry.internal_port, portCandidates)
|
|
4025
|
-
collectPort(entry.external_port, portCandidates)
|
|
4026
|
-
|
|
4027
|
-
if (hostPortCandidates.size === 0 && portCandidates.size === 0) {
|
|
4028
|
-
httpsTargets.forEach((target) => {
|
|
4029
|
-
collectHostPort(target, hostPortCandidates, portCandidates)
|
|
4030
|
-
})
|
|
4031
|
-
}
|
|
4032
|
-
|
|
4033
|
-
if (hostPortCandidates.size === 0 && portCandidates.size === 0) {
|
|
4034
|
-
return
|
|
4035
|
-
}
|
|
4036
|
-
|
|
4037
|
-
hostPortCandidates.forEach((key) => {
|
|
4038
|
-
const parsed = parseHostPort(key)
|
|
4039
|
-
if (parsed) {
|
|
4040
|
-
addHttpMapping(parsed.host, parsed.port, httpsTargets)
|
|
4041
|
-
}
|
|
4042
|
-
})
|
|
4043
|
-
|
|
4044
|
-
portCandidates.forEach((port) => {
|
|
4045
|
-
localAliases.forEach((host) => {
|
|
4046
|
-
addHttpMapping(host, port, httpsTargets)
|
|
4047
|
-
})
|
|
4048
|
-
})
|
|
4049
|
-
}
|
|
4050
|
-
|
|
4051
|
-
const visited = new WeakSet()
|
|
4052
|
-
const traverseNode = (node) => {
|
|
4053
|
-
if (!node) {
|
|
4054
|
-
return
|
|
4055
|
-
}
|
|
4056
|
-
if (Array.isArray(node)) {
|
|
4057
|
-
node.forEach(traverseNode)
|
|
4058
|
-
return
|
|
4059
|
-
}
|
|
4060
|
-
if (typeof node !== "object") {
|
|
4061
|
-
return
|
|
4062
|
-
}
|
|
4063
|
-
if (visited.has(node)) {
|
|
4064
|
-
return
|
|
4065
|
-
}
|
|
4066
|
-
visited.add(node)
|
|
4067
|
-
registerEntry(node)
|
|
4068
|
-
Object.values(node).forEach((value) => {
|
|
4069
|
-
if (value && typeof value === "object") {
|
|
4070
|
-
traverseNode(value)
|
|
4071
|
-
}
|
|
4072
|
-
})
|
|
4073
|
-
}
|
|
4074
|
-
|
|
4075
|
-
processes.forEach(traverseNode)
|
|
4076
|
-
rewriteMapping.forEach(traverseNode)
|
|
4077
|
-
|
|
4078
|
-
return {
|
|
4079
|
-
portMap,
|
|
4080
|
-
hostPortMap,
|
|
4081
|
-
externalHttpByExtPort,
|
|
4082
|
-
externalHttpByIntPort
|
|
4083
|
-
}
|
|
4084
|
-
})
|
|
4085
|
-
.catch(() => {
|
|
4086
|
-
tabLinkRouterHttpsActive = null
|
|
4087
|
-
return {
|
|
4088
|
-
portMap: new Map(),
|
|
4089
|
-
hostPortMap: new Map(),
|
|
4090
|
-
externalHttpByExtPort: new Map(),
|
|
4091
|
-
externalHttpByIntPort: new Map()
|
|
4092
|
-
}
|
|
4093
|
-
})
|
|
4094
|
-
tabLinkRouterInfoExpiry = now + 3000
|
|
4095
|
-
}
|
|
4096
|
-
return tabLinkRouterInfoPromise
|
|
4097
|
-
}
|
|
4098
|
-
|
|
4099
|
-
const collectHttpsUrlsFromRouter = (httpUrl, routerData) => {
|
|
4100
|
-
if (!routerData) {
|
|
4101
|
-
return []
|
|
4102
|
-
}
|
|
4103
|
-
let parsed
|
|
4104
|
-
try {
|
|
4105
|
-
parsed = new URL(httpUrl, location.origin)
|
|
4106
|
-
} catch (_) {
|
|
4107
|
-
return []
|
|
4108
|
-
}
|
|
4109
|
-
const protocol = parsed.protocol.toLowerCase()
|
|
4110
|
-
if (protocol !== "http:" && protocol !== "https:") {
|
|
4111
|
-
return []
|
|
4112
|
-
}
|
|
4113
|
-
let port = parsed.port
|
|
4114
|
-
if (!port) {
|
|
4115
|
-
if (protocol === "http:") {
|
|
4116
|
-
port = "80"
|
|
4117
|
-
} else if (protocol === "https:") {
|
|
4118
|
-
port = "443"
|
|
4119
|
-
}
|
|
4120
|
-
}
|
|
4121
|
-
const hostLower = parsed.hostname.toLowerCase()
|
|
4122
|
-
const results = new Set()
|
|
4123
|
-
if (port) {
|
|
4124
|
-
const hostPortKey = `${hostLower}:${port}`
|
|
4125
|
-
if (routerData.hostPortMap.has(hostPortKey)) {
|
|
4126
|
-
routerData.hostPortMap.get(hostPortKey).forEach((value) => results.add(value))
|
|
4127
|
-
}
|
|
4128
|
-
if (routerData.portMap.has(port)) {
|
|
4129
|
-
routerData.portMap.get(port).forEach((value) => results.add(value))
|
|
4130
|
-
}
|
|
4131
|
-
}
|
|
4132
|
-
return Array.from(results)
|
|
4133
|
-
}
|
|
4134
|
-
|
|
4135
|
-
const buildTabLinkEntries = async (link) => {
|
|
4136
|
-
if (!link || !link.href) {
|
|
4137
|
-
return []
|
|
4138
|
-
}
|
|
4139
|
-
|
|
4140
|
-
const baseHref = link.href
|
|
4141
|
-
let canonicalBase = canonicalizeUrl(baseHref)
|
|
4142
|
-
if (canonicalBase && isHttpUrl(canonicalBase)) {
|
|
4143
|
-
canonicalBase = ensureHttpDirectoryUrl(canonicalBase)
|
|
4144
|
-
}
|
|
4145
|
-
let parsedBaseUrl = null
|
|
4146
|
-
let sameOrigin = false
|
|
4147
|
-
let basePortNormalized = ""
|
|
4148
|
-
try {
|
|
4149
|
-
parsedBaseUrl = new URL(baseHref, location.origin)
|
|
4150
|
-
sameOrigin = parsedBaseUrl.origin === location.origin
|
|
4151
|
-
if (parsedBaseUrl) {
|
|
4152
|
-
basePortNormalized = parsedBaseUrl.port
|
|
4153
|
-
if (!basePortNormalized) {
|
|
4154
|
-
const proto = parsedBaseUrl.protocol ? parsedBaseUrl.protocol.toLowerCase() : "http:"
|
|
4155
|
-
basePortNormalized = proto === "https:" ? "443" : "80"
|
|
4156
|
-
}
|
|
4157
|
-
}
|
|
4158
|
-
} catch (_) {}
|
|
4159
|
-
const projectSlug = extractProjectSlug(link).toLowerCase()
|
|
4160
|
-
const entries = []
|
|
4161
|
-
const entryByUrl = new Map()
|
|
4162
|
-
const addEntry = (type, label, url, opts = {}) => {
|
|
4163
|
-
if (!url) {
|
|
4164
|
-
return
|
|
4165
|
-
}
|
|
4166
|
-
let canonical = canonicalizeUrl(url)
|
|
4167
|
-
if (canonical && type === "http") {
|
|
4168
|
-
canonical = ensureHttpDirectoryUrl(canonical)
|
|
4169
|
-
}
|
|
4170
|
-
if (!canonical) {
|
|
4171
|
-
return
|
|
4172
|
-
}
|
|
4173
|
-
let skip = false
|
|
4174
|
-
const allowSameOrigin = opts && opts.allowSameOrigin === true
|
|
4175
|
-
try {
|
|
4176
|
-
const parsed = new URL(canonical)
|
|
4177
|
-
const originLower = parsed.origin.toLowerCase()
|
|
4178
|
-
if (!allowSameOrigin && originLower === location.origin.toLowerCase()) {
|
|
4179
|
-
skip = true
|
|
4180
|
-
}
|
|
4181
|
-
} catch (_) {
|
|
4182
|
-
// ignore parse failures but do not skip by default
|
|
4183
|
-
}
|
|
4184
|
-
if (skip) {
|
|
4185
|
-
return
|
|
4186
|
-
}
|
|
4187
|
-
if (entryByUrl.has(canonical)) {
|
|
4188
|
-
const existing = entryByUrl.get(canonical)
|
|
4189
|
-
if (opts && opts.qr === true) existing.qr = true
|
|
4190
|
-
return
|
|
4191
|
-
}
|
|
4192
|
-
const entry = {
|
|
4193
|
-
type,
|
|
4194
|
-
label,
|
|
4195
|
-
url: canonical,
|
|
4196
|
-
display: formatDisplayUrl(canonical),
|
|
4197
|
-
qr: opts && opts.qr === true
|
|
4198
|
-
}
|
|
4199
|
-
entryByUrl.set(canonical, entry)
|
|
4200
|
-
entries.push(entry)
|
|
4201
|
-
}
|
|
4202
|
-
|
|
4203
|
-
if (isHttpUrl(baseHref)) {
|
|
4204
|
-
addEntry("http", "HTTP", baseHref, { allowSameOrigin: true })
|
|
4205
|
-
} else if (isHttpsUrl(baseHref)) {
|
|
4206
|
-
addEntry("https", "HTTPS", baseHref, { allowSameOrigin: true })
|
|
4207
|
-
} else {
|
|
4208
|
-
addEntry("url", "URL", baseHref, { allowSameOrigin: true })
|
|
4209
|
-
}
|
|
4210
|
-
|
|
4211
|
-
const httpCandidates = new Map() // url -> { qr: boolean }
|
|
4212
|
-
const httpsCandidates = new Set()
|
|
4213
|
-
|
|
4214
|
-
if (isHttpUrl(baseHref)) {
|
|
4215
|
-
httpCandidates.set(canonicalBase || canonicalizeUrl(baseHref), { qr: false })
|
|
4216
|
-
} else if (isHttpsUrl(baseHref)) {
|
|
4217
|
-
if (canonicalBase) {
|
|
4218
|
-
httpsCandidates.add(canonicalBase)
|
|
4219
|
-
} else {
|
|
4220
|
-
httpsCandidates.add(canonicalizeUrl(baseHref))
|
|
4221
|
-
}
|
|
4222
|
-
}
|
|
4223
|
-
|
|
4224
|
-
if (projectSlug) {
|
|
4225
|
-
try {
|
|
4226
|
-
const baseUrl = parsedBaseUrl || new URL(baseHref, location.origin)
|
|
4227
|
-
let pathname = baseUrl.pathname || "/"
|
|
4228
|
-
if (pathname.endsWith("/index.html")) {
|
|
4229
|
-
pathname = pathname.slice(0, -"/index.html".length)
|
|
4230
|
-
}
|
|
4231
|
-
if (!pathname.endsWith("/")) {
|
|
4232
|
-
pathname = `${pathname}/`
|
|
4233
|
-
}
|
|
4234
|
-
const normalizedPath = pathname.toLowerCase()
|
|
4235
|
-
if (normalizedPath.includes(`/asset/api/${projectSlug}`)) {
|
|
4236
|
-
const fallbackHttp = `http://127.0.0.1:42000${pathname}`
|
|
4237
|
-
httpCandidates.set(canonicalizeUrl(fallbackHttp), { qr: false })
|
|
4238
|
-
}
|
|
4239
|
-
} catch (_) {
|
|
4240
|
-
// ignore fallback errors
|
|
4241
|
-
}
|
|
4242
|
-
}
|
|
4243
|
-
|
|
4244
|
-
const scriptKeys = collectScriptKeys(link)
|
|
4245
|
-
if (scriptKeys.length > 0) {
|
|
4246
|
-
const localInfo = await ensureLocalMemory()
|
|
4247
|
-
scriptKeys.forEach((key) => {
|
|
4248
|
-
if (!key) {
|
|
4249
|
-
return
|
|
4250
|
-
}
|
|
4251
|
-
const local = localInfo ? localInfo[key] : undefined
|
|
4252
|
-
if (!local) {
|
|
4253
|
-
return
|
|
4254
|
-
}
|
|
4255
|
-
const urls = collectUrlsFromLocal(local)
|
|
4256
|
-
urls.forEach((value) => {
|
|
4257
|
-
const canonical = canonicalizeUrl(value)
|
|
4258
|
-
if (isHttpsUrl(canonical)) {
|
|
4259
|
-
httpsCandidates.add(canonical)
|
|
4260
|
-
} else if (isHttpUrl(canonical)) {
|
|
4261
|
-
const prev = httpCandidates.get(canonical)
|
|
4262
|
-
httpCandidates.set(canonical, { qr: prev ? prev.qr === true : false })
|
|
4263
|
-
}
|
|
4264
|
-
})
|
|
4265
|
-
})
|
|
4266
|
-
}
|
|
4267
|
-
|
|
4268
|
-
const routerData = await ensureRouterInfoMapping()
|
|
4269
|
-
if (httpCandidates.size > 0) {
|
|
4270
|
-
Array.from(httpCandidates.keys()).forEach((httpUrl) => {
|
|
4271
|
-
const mapped = collectHttpsUrlsFromRouter(httpUrl, routerData)
|
|
4272
|
-
mapped.forEach((httpsUrl) => {
|
|
4273
|
-
httpsCandidates.add(httpsUrl)
|
|
4274
|
-
})
|
|
4275
|
-
})
|
|
4276
|
-
}
|
|
4277
|
-
|
|
4278
|
-
// Add external 192.168.* http host:port candidates mapped from the same internal port as base HTTP
|
|
4279
|
-
try {
|
|
4280
|
-
const base = parsedBaseUrl || new URL(baseHref, location.origin)
|
|
4281
|
-
let basePort = base.port
|
|
4282
|
-
if (!basePort) {
|
|
4283
|
-
basePort = base.protocol.toLowerCase() === 'https:' ? '443' : '80'
|
|
4284
|
-
}
|
|
4285
|
-
const samePortHosts = routerData && routerData.externalHttpByIntPort ? routerData.externalHttpByIntPort.get(basePort) : null
|
|
4286
|
-
if (samePortHosts && samePortHosts.size > 0) {
|
|
4287
|
-
samePortHosts.forEach((hostport) => {
|
|
4288
|
-
try {
|
|
4289
|
-
const hpUrl = `http://${hostport}${base.pathname || '/'}${base.search || ''}`
|
|
4290
|
-
const canonical = canonicalizeUrl(hpUrl)
|
|
4291
|
-
if (isHttpUrl(canonical)) {
|
|
4292
|
-
const prev = httpCandidates.get(canonical)
|
|
4293
|
-
httpCandidates.set(canonical, { qr: true || (prev ? prev.qr === true : false) })
|
|
4294
|
-
}
|
|
4295
|
-
} catch (_) {}
|
|
4296
|
-
})
|
|
4297
|
-
}
|
|
4298
|
-
} catch (_) {}
|
|
4299
|
-
|
|
4300
|
-
const httpsList = Array.from(httpsCandidates).sort()
|
|
4301
|
-
|
|
4302
|
-
if (httpsList.length > 0) {
|
|
4303
|
-
httpsList.forEach((url) => {
|
|
4304
|
-
try {
|
|
4305
|
-
const parsed = new URL(url)
|
|
4306
|
-
if (parsed.protocol.toLowerCase() !== "https:") {
|
|
4307
|
-
return
|
|
4308
|
-
}
|
|
4309
|
-
if (!parsed.port || parsed.port !== "42000") {
|
|
4310
|
-
return
|
|
4311
|
-
}
|
|
4312
|
-
const hostPort = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname
|
|
4313
|
-
const httpUrl = `http://${hostPort}${parsed.pathname || "/"}${parsed.search || ""}`
|
|
4314
|
-
const key = canonicalizeUrl(httpUrl)
|
|
4315
|
-
const prev = httpCandidates.get(key)
|
|
4316
|
-
httpCandidates.set(key, { qr: prev ? prev.qr === true : false })
|
|
4317
|
-
} catch (_) {
|
|
4318
|
-
// ignore failures
|
|
4319
|
-
}
|
|
4320
|
-
})
|
|
4321
|
-
}
|
|
4322
|
-
|
|
4323
|
-
const httpList = Array.from(httpCandidates.keys()).sort()
|
|
4324
|
-
|
|
4325
|
-
httpList.forEach((url) => {
|
|
4326
|
-
const meta = httpCandidates.get(url) || { qr: false }
|
|
4327
|
-
addEntry("http", "HTTP", url, { qr: meta.qr === true })
|
|
4328
|
-
})
|
|
4329
|
-
httpsList.forEach((url) => {
|
|
4330
|
-
addEntry("https", "HTTPS", url)
|
|
4331
|
-
})
|
|
4332
|
-
|
|
4333
|
-
if (sameOrigin) {
|
|
4334
|
-
try {
|
|
4335
|
-
const peerInfo = await ensurePeerInfo()
|
|
4336
|
-
const peerHost = peerInfo && typeof peerInfo.host === "string" ? peerInfo.host.trim() : ""
|
|
4337
|
-
if (peerHost) {
|
|
4338
|
-
const peerHostLower = peerHost.toLowerCase()
|
|
4339
|
-
// Skip loopback-style peers
|
|
4340
|
-
if (peerHostLower !== "localhost" && !peerHostLower.startsWith("127.")) {
|
|
4341
|
-
const baseUrl = parsedBaseUrl || new URL(baseHref, location.origin)
|
|
4342
|
-
const baseHostLower = (baseUrl.hostname || "").toLowerCase()
|
|
4343
|
-
if (peerHostLower !== baseHostLower) {
|
|
4344
|
-
const baseProtocol = baseUrl.protocol ? baseUrl.protocol.toLowerCase() : "http:"
|
|
4345
|
-
const scheme = baseProtocol === "https:" ? "https://" : "http://"
|
|
4346
|
-
const port = baseUrl.port || (baseProtocol === "https:" ? "443" : "80")
|
|
4347
|
-
const hostPort = port ? `${peerHostLower}:${port}` : peerHostLower
|
|
4348
|
-
const pathSegment = baseUrl.pathname || "/"
|
|
4349
|
-
const searchSegment = baseUrl.search || ""
|
|
4350
|
-
const fallbackUrl = `${scheme}${hostPort}${pathSegment}${searchSegment}`
|
|
4351
|
-
const label = baseProtocol === "https:" ? "HTTPS" : "HTTP"
|
|
4352
|
-
addEntry(baseProtocol === "https:" ? "https" : "http", label, fallbackUrl, { qr: true })
|
|
4353
|
-
}
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
} catch (_) {}
|
|
4357
|
-
|
|
4358
|
-
const matchesBasePort = (value) => {
|
|
4359
|
-
if (!basePortNormalized) {
|
|
4360
|
-
return true
|
|
4361
|
-
}
|
|
4362
|
-
try {
|
|
4363
|
-
const parsed = new URL(value, location.origin)
|
|
4364
|
-
let port = parsed.port
|
|
4365
|
-
if (!port) {
|
|
4366
|
-
const proto = parsed.protocol ? parsed.protocol.toLowerCase() : "http:"
|
|
4367
|
-
port = proto === "https:" ? "443" : "80"
|
|
4368
|
-
}
|
|
4369
|
-
return port === basePortNormalized
|
|
4370
|
-
} catch (_) {
|
|
4371
|
-
return false
|
|
4372
|
-
}
|
|
4373
|
-
}
|
|
4374
|
-
|
|
4375
|
-
const filteredEntries = entries.filter((entry) => {
|
|
4376
|
-
if (!entry || !entry.url) {
|
|
4377
|
-
return false
|
|
4378
|
-
}
|
|
4379
|
-
if (entry.url === canonicalBase) {
|
|
4380
|
-
return true
|
|
4381
|
-
}
|
|
4382
|
-
if (entry.qr === true) {
|
|
4383
|
-
return matchesBasePort(entry.url)
|
|
4384
|
-
}
|
|
4385
|
-
return false
|
|
4386
|
-
})
|
|
4387
|
-
if (filteredEntries.length > 0) {
|
|
4388
|
-
return filteredEntries
|
|
4389
|
-
}
|
|
4390
|
-
}
|
|
4391
|
-
|
|
4392
|
-
return entries
|
|
4393
|
-
}
|
|
4394
|
-
|
|
4395
|
-
const positionTabLinkPopover = (popover, link) => {
|
|
4396
|
-
if (!popover || !link) {
|
|
4397
|
-
return
|
|
4398
|
-
}
|
|
4399
|
-
const rect = link.getBoundingClientRect()
|
|
4400
|
-
const minWidth = Math.max(rect.width, 260)
|
|
4401
|
-
popover.style.minWidth = `${Math.round(minWidth)}px`
|
|
4402
|
-
popover.style.display = "flex"
|
|
4403
|
-
popover.classList.add("visible")
|
|
4404
|
-
popover.style.visibility = "hidden"
|
|
4405
|
-
|
|
4406
|
-
const popoverWidth = popover.offsetWidth
|
|
4407
|
-
const popoverHeight = popover.offsetHeight
|
|
4408
|
-
|
|
4409
|
-
let left = rect.left
|
|
4410
|
-
let top = rect.bottom + 8
|
|
4411
|
-
|
|
4412
|
-
if (left + popoverWidth > window.innerWidth - 12) {
|
|
4413
|
-
left = window.innerWidth - popoverWidth - 12
|
|
4414
|
-
}
|
|
4415
|
-
if (left < 12) {
|
|
4416
|
-
left = 12
|
|
4417
|
-
}
|
|
4418
|
-
|
|
4419
|
-
if (top + popoverHeight > window.innerHeight - 12) {
|
|
4420
|
-
top = Math.max(12, rect.top - popoverHeight - 8)
|
|
4421
|
-
}
|
|
4422
|
-
|
|
4423
|
-
popover.style.left = `${Math.round(left)}px`
|
|
4424
|
-
popover.style.top = `${Math.round(top)}px`
|
|
4425
|
-
popover.style.visibility = ""
|
|
4426
|
-
}
|
|
4427
|
-
|
|
4428
|
-
const hideTabLinkPopover = ({ immediate = false } = {}) => {
|
|
4429
|
-
const applyHide = () => {
|
|
4430
|
-
if (tabLinkPopoverEl) {
|
|
4431
|
-
tabLinkPopoverEl.classList.remove("visible")
|
|
4432
|
-
tabLinkPopoverEl.style.display = "none"
|
|
4433
|
-
}
|
|
4434
|
-
tabLinkActiveLink = null
|
|
4435
|
-
tabLinkPendingLink = null
|
|
4436
|
-
tabLinkHideTimer = null
|
|
4437
|
-
}
|
|
4438
|
-
|
|
4439
|
-
if (tabLinkHideTimer) {
|
|
4440
|
-
clearTimeout(tabLinkHideTimer)
|
|
4441
|
-
tabLinkHideTimer = null
|
|
4442
|
-
}
|
|
4443
|
-
|
|
4444
|
-
if (immediate) {
|
|
4445
|
-
applyHide()
|
|
4446
|
-
} else {
|
|
4447
|
-
tabLinkHideTimer = setTimeout(applyHide, 120)
|
|
4448
|
-
}
|
|
4449
|
-
}
|
|
4450
|
-
|
|
4451
|
-
const renderTabLinkPopover = async (link) => {
|
|
4452
|
-
if (!link || !link.href) {
|
|
4453
|
-
hideTabLinkPopover({ immediate: true })
|
|
4454
|
-
return
|
|
4455
|
-
}
|
|
4456
|
-
|
|
4457
|
-
let sameOrigin = false
|
|
4458
|
-
let canonicalBase = canonicalizeUrl(link.href)
|
|
4459
|
-
if (canonicalBase && isHttpUrl(canonicalBase)) {
|
|
4460
|
-
canonicalBase = ensureHttpDirectoryUrl(canonicalBase)
|
|
4461
|
-
}
|
|
4462
|
-
let basePortNormalized = ""
|
|
4463
|
-
try {
|
|
4464
|
-
const linkUrl = new URL(link.href, location.href)
|
|
4465
|
-
sameOrigin = linkUrl.origin === location.origin
|
|
4466
|
-
canonicalBase = canonicalizeUrl(linkUrl.href)
|
|
4467
|
-
if (canonicalBase && isHttpUrl(canonicalBase)) {
|
|
4468
|
-
canonicalBase = ensureHttpDirectoryUrl(canonicalBase)
|
|
4469
|
-
}
|
|
4470
|
-
basePortNormalized = linkUrl.port
|
|
4471
|
-
if (!basePortNormalized) {
|
|
4472
|
-
const proto = linkUrl.protocol ? linkUrl.protocol.toLowerCase() : "http:"
|
|
4473
|
-
basePortNormalized = proto === "https:" ? "443" : "80"
|
|
4474
|
-
}
|
|
4475
|
-
} catch (_) {
|
|
4476
|
-
hideTabLinkPopover({ immediate: true })
|
|
4477
|
-
return
|
|
4478
|
-
}
|
|
4479
|
-
|
|
4480
|
-
if (tabLinkActiveLink === link && tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible")) {
|
|
4481
|
-
if (tabLinkHideTimer) {
|
|
4482
|
-
clearTimeout(tabLinkHideTimer)
|
|
4483
|
-
tabLinkHideTimer = null
|
|
4484
|
-
}
|
|
4485
|
-
return
|
|
4486
|
-
}
|
|
4487
|
-
|
|
4488
|
-
if (tabLinkPendingLink === link && tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible")) {
|
|
4489
|
-
return
|
|
4490
|
-
}
|
|
4491
|
-
|
|
4492
|
-
tabLinkPendingLink = link
|
|
4493
|
-
if (tabLinkHideTimer) {
|
|
4494
|
-
clearTimeout(tabLinkHideTimer)
|
|
4495
|
-
tabLinkHideTimer = null
|
|
4496
|
-
}
|
|
4497
|
-
|
|
4498
|
-
// Show lightweight loading popover immediately while mapping fetch runs
|
|
4499
|
-
try {
|
|
4500
|
-
const pop = ensureTabLinkPopoverEl()
|
|
4501
|
-
pop.innerHTML = ''
|
|
4502
|
-
const header = document.createElement('div')
|
|
4503
|
-
header.className = 'tab-link-popover-header'
|
|
4504
|
-
header.innerHTML = `<i class="fa-solid fa-arrow-up-right-from-square"></i><span>Open in browser</span>`
|
|
4505
|
-
const item = document.createElement('div')
|
|
4506
|
-
item.className = 'tab-link-popover-item'
|
|
4507
|
-
const label = document.createElement('span')
|
|
4508
|
-
label.className = 'label'
|
|
4509
|
-
label.textContent = 'Loading…'
|
|
4510
|
-
const value = document.createElement('span')
|
|
4511
|
-
value.className = 'value muted'
|
|
4512
|
-
value.textContent = 'Discovering routes'
|
|
4513
|
-
item.append(label, value)
|
|
4514
|
-
pop.append(header, item)
|
|
4515
|
-
positionTabLinkPopover(pop, link)
|
|
4516
|
-
} catch (_) {}
|
|
4517
|
-
|
|
4518
|
-
let entries
|
|
4519
|
-
try {
|
|
4520
|
-
entries = await buildTabLinkEntries(link)
|
|
4521
|
-
} catch (_) {
|
|
4522
|
-
tabLinkPendingLink = null
|
|
4523
|
-
return
|
|
4524
|
-
}
|
|
4525
|
-
|
|
4526
|
-
if (tabLinkPendingLink !== link) {
|
|
4527
|
-
return
|
|
4528
|
-
}
|
|
4529
|
-
|
|
4530
|
-
if (!entries || entries.length === 0) {
|
|
4531
|
-
hideTabLinkPopover({ immediate: true })
|
|
4532
|
-
return
|
|
4533
|
-
}
|
|
4534
|
-
|
|
4535
|
-
if (sameOrigin) {
|
|
4536
|
-
const slug = extractProjectSlug(link).toLowerCase()
|
|
4537
|
-
const matchesBasePort = (value) => {
|
|
4538
|
-
if (!basePortNormalized) {
|
|
4539
|
-
return true
|
|
4540
|
-
}
|
|
4541
|
-
try {
|
|
4542
|
-
const parsed = new URL(value, location.origin)
|
|
4543
|
-
let port = parsed.port
|
|
4544
|
-
if (!port) {
|
|
4545
|
-
const proto = parsed.protocol ? parsed.protocol.toLowerCase() : "http:"
|
|
4546
|
-
port = proto === "https:" ? "443" : "80"
|
|
4547
|
-
}
|
|
4548
|
-
return port === basePortNormalized
|
|
4549
|
-
} catch (_) {
|
|
4550
|
-
return false
|
|
4551
|
-
}
|
|
4552
|
-
}
|
|
4553
|
-
|
|
4554
|
-
if (slug) {
|
|
4555
|
-
entries = entries.filter((entry) => {
|
|
4556
|
-
if (!entry || !entry.url) {
|
|
4557
|
-
return false
|
|
4558
|
-
}
|
|
4559
|
-
if (entry.url === canonicalBase) {
|
|
4560
|
-
return true
|
|
4561
|
-
}
|
|
4562
|
-
if (entry.qr === true) {
|
|
4563
|
-
return matchesBasePort(entry.url)
|
|
4564
|
-
}
|
|
4565
|
-
try {
|
|
4566
|
-
const parsed = new URL(entry.url)
|
|
4567
|
-
const hostLower = parsed.hostname ? parsed.hostname.toLowerCase() : ""
|
|
4568
|
-
if (isLocalHostLike(hostLower)) {
|
|
4569
|
-
if (entry.type === "http") {
|
|
4570
|
-
const pathLower = parsed.pathname ? parsed.pathname.toLowerCase() : ""
|
|
4571
|
-
if (pathLower.includes(`/asset/api/${slug}`) || pathLower.includes(`/p/${slug}`)) {
|
|
4572
|
-
return true
|
|
4573
|
-
}
|
|
4574
|
-
}
|
|
4575
|
-
return false
|
|
4576
|
-
}
|
|
4577
|
-
const pathLower = parsed.pathname ? parsed.pathname.toLowerCase() : ""
|
|
4578
|
-
if (pathLower.includes(`/asset/api/${slug}`)) {
|
|
4579
|
-
return true
|
|
4580
|
-
}
|
|
4581
|
-
if (pathLower.includes(`/p/${slug}`)) {
|
|
4582
|
-
return true
|
|
4583
|
-
}
|
|
4584
|
-
if (hostLower.split(".").some((part) => part === slug)) {
|
|
4585
|
-
return true
|
|
4586
|
-
}
|
|
4587
|
-
} catch (_) {
|
|
4588
|
-
return false
|
|
4589
|
-
}
|
|
4590
|
-
return false
|
|
4591
|
-
})
|
|
4592
|
-
} else {
|
|
4593
|
-
entries = entries.filter((entry) => {
|
|
4594
|
-
if (!entry || !entry.url) {
|
|
4595
|
-
return false
|
|
4596
|
-
}
|
|
4597
|
-
if (entry.url === canonicalBase) {
|
|
4598
|
-
return true
|
|
4599
|
-
}
|
|
4600
|
-
if (entry.qr === true) {
|
|
4601
|
-
return matchesBasePort(entry.url)
|
|
4602
|
-
}
|
|
4603
|
-
return false
|
|
4604
|
-
})
|
|
4605
|
-
}
|
|
4606
|
-
|
|
4607
|
-
entries = entries.filter((entry) => {
|
|
4608
|
-
if (!entry || !entry.url) {
|
|
4609
|
-
return false
|
|
4610
|
-
}
|
|
4611
|
-
if (entry.url === canonicalBase) {
|
|
4612
|
-
return true
|
|
4613
|
-
}
|
|
4614
|
-
if (entry.qr === true) {
|
|
4615
|
-
return matchesBasePort(entry.url)
|
|
4616
|
-
}
|
|
4617
|
-
return false
|
|
4618
|
-
})
|
|
4619
|
-
|
|
4620
|
-
const hasAlternate = entries.some((entry) => entry.url !== canonicalBase)
|
|
4621
|
-
if (!hasAlternate) {
|
|
4622
|
-
hideTabLinkPopover({ immediate: true })
|
|
4623
|
-
return
|
|
4624
|
-
}
|
|
4625
|
-
}
|
|
4626
|
-
|
|
4627
|
-
if (!entries || entries.length === 0) {
|
|
4628
|
-
hideTabLinkPopover({ immediate: true })
|
|
4629
|
-
return
|
|
4630
|
-
}
|
|
4631
|
-
|
|
4632
|
-
const popover = ensureTabLinkPopoverEl()
|
|
4633
|
-
popover.innerHTML = ""
|
|
4634
|
-
|
|
4635
|
-
const header = document.createElement("div")
|
|
4636
|
-
header.className = "tab-link-popover-header"
|
|
4637
|
-
header.innerHTML = `<i class="fa-solid fa-arrow-up-right-from-square"></i><span>Open in browser</span>`
|
|
4638
|
-
popover.appendChild(header)
|
|
4639
|
-
|
|
4640
|
-
const hasHttpsEntry = entries.some((entry) => entry && entry.type === "https")
|
|
4641
|
-
|
|
4642
|
-
entries.forEach((entry) => {
|
|
4643
|
-
const item = document.createElement("button")
|
|
4644
|
-
item.type = "button"
|
|
4645
|
-
item.setAttribute("data-url", entry.url)
|
|
4646
|
-
const labelSpan = document.createElement("span")
|
|
4647
|
-
labelSpan.className = "label"
|
|
4648
|
-
labelSpan.textContent = entry.label
|
|
4649
|
-
const valueSpan = document.createElement("span")
|
|
4650
|
-
valueSpan.className = "value"
|
|
4651
|
-
valueSpan.textContent = entry.display
|
|
4652
|
-
|
|
4653
|
-
if (entry.type === 'http' && entry.qr === true) {
|
|
4654
|
-
item.className = "tab-link-popover-item qr-inline"
|
|
4655
|
-
const textCol = document.createElement('div')
|
|
4656
|
-
textCol.className = 'textcol'
|
|
4657
|
-
textCol.append(labelSpan, valueSpan)
|
|
4658
|
-
const qrImg = document.createElement('img')
|
|
4659
|
-
qrImg.className = 'qr'
|
|
4660
|
-
qrImg.alt = 'QR'
|
|
4661
|
-
qrImg.decoding = 'async'
|
|
4662
|
-
qrImg.loading = 'lazy'
|
|
4663
|
-
qrImg.src = `/qr?data=${encodeURIComponent(entry.url)}&s=4&m=0`
|
|
4664
|
-
item.append(textCol, qrImg)
|
|
4665
|
-
} else {
|
|
4666
|
-
item.className = "tab-link-popover-item"
|
|
4667
|
-
// Keep label and value as direct children so column layout applies
|
|
4668
|
-
item.append(labelSpan, valueSpan)
|
|
4669
|
-
}
|
|
4670
|
-
popover.appendChild(item)
|
|
4671
|
-
})
|
|
4672
|
-
|
|
4673
|
-
if (tabLinkRouterHttpsActive === false && !hasHttpsEntry) {
|
|
4674
|
-
const footerButton = document.createElement("button")
|
|
4675
|
-
footerButton.type = "button"
|
|
4676
|
-
footerButton.className = "tab-link-popover-item tab-link-popover-footer"
|
|
4677
|
-
footerButton.setAttribute("data-url", "/network")
|
|
4678
|
-
footerButton.setAttribute("data-target", "_self")
|
|
4679
|
-
footerButton.setAttribute("aria-label", "Open network settings to configure local HTTPS")
|
|
4680
|
-
|
|
4681
|
-
const footerLabel = document.createElement("span")
|
|
4682
|
-
footerLabel.className = "label"
|
|
4683
|
-
footerLabel.textContent = "Custom domain not active"
|
|
4684
|
-
|
|
4685
|
-
const footerValue = document.createElement("span")
|
|
4686
|
-
footerValue.className = "value"
|
|
4687
|
-
footerValue.textContent = "Click to activate"
|
|
4688
|
-
|
|
4689
|
-
footerButton.append(footerLabel, footerValue)
|
|
4690
|
-
popover.appendChild(footerButton)
|
|
4691
|
-
}
|
|
4692
|
-
|
|
4693
|
-
tabLinkActiveLink = link
|
|
4694
|
-
tabLinkPendingLink = null
|
|
4695
|
-
positionTabLinkPopover(popover, link)
|
|
4696
|
-
}
|
|
4697
|
-
|
|
4698
|
-
const setupTabLinkHover = () => {
|
|
4699
|
-
const container = document.querySelector(".appcanvas > aside .menu-container")
|
|
4700
|
-
if (!container) {
|
|
4701
|
-
return
|
|
4702
|
-
}
|
|
4703
|
-
|
|
4704
|
-
container.addEventListener("mouseover", (event) => {
|
|
4705
|
-
const link = event.target.closest(".frame-link")
|
|
4706
|
-
if (!link || !container.contains(link)) {
|
|
4707
|
-
return
|
|
4708
|
-
}
|
|
4709
|
-
renderTabLinkPopover(link)
|
|
4710
|
-
})
|
|
4711
|
-
|
|
4712
|
-
container.addEventListener("mouseout", (event) => {
|
|
4713
|
-
const origin = event.target.closest(".frame-link")
|
|
4714
|
-
if (!origin || !container.contains(origin)) {
|
|
4715
|
-
return
|
|
4716
|
-
}
|
|
4717
|
-
const related = event.relatedTarget
|
|
4718
|
-
const popover = tabLinkPopoverEl || document.getElementById(TAB_LINK_POPOVER_ID)
|
|
4719
|
-
if (related && (origin.contains(related) || (popover && popover.contains(related)))) {
|
|
4720
|
-
return
|
|
4721
|
-
}
|
|
4722
|
-
hideTabLinkPopover()
|
|
4723
|
-
})
|
|
4724
|
-
}
|
|
4725
|
-
|
|
4726
|
-
const handleGlobalPointer = (event) => {
|
|
4727
|
-
if (!tabLinkPopoverEl || !tabLinkPopoverEl.classList.contains("visible")) {
|
|
4728
|
-
return
|
|
4729
|
-
}
|
|
4730
|
-
if (tabLinkPopoverEl.contains(event.target)) {
|
|
4731
|
-
return
|
|
4732
|
-
}
|
|
4733
|
-
if (tabLinkActiveLink && tabLinkActiveLink.contains(event.target)) {
|
|
4734
|
-
return
|
|
4735
|
-
}
|
|
4736
|
-
hideTabLinkPopover({ immediate: true })
|
|
4737
|
-
}
|
|
4738
|
-
|
|
4739
|
-
window.addEventListener("scroll", () => {
|
|
4740
|
-
if (tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible")) {
|
|
4741
|
-
hideTabLinkPopover({ immediate: true })
|
|
4742
|
-
}
|
|
4743
|
-
}, true)
|
|
4744
|
-
|
|
4745
|
-
window.addEventListener("resize", () => {
|
|
4746
|
-
if (tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible") && tabLinkActiveLink) {
|
|
4747
|
-
positionTabLinkPopover(tabLinkPopoverEl, tabLinkActiveLink)
|
|
4748
|
-
}
|
|
4749
|
-
})
|
|
4750
|
-
|
|
4751
|
-
document.addEventListener("mousedown", handleGlobalPointer, true)
|
|
4752
|
-
try {
|
|
4753
|
-
document.addEventListener("touchstart", handleGlobalPointer, { passive: true, capture: true })
|
|
4754
|
-
} catch (_) {
|
|
4755
|
-
document.addEventListener("touchstart", handleGlobalPointer, true)
|
|
4756
|
-
}
|
|
4757
|
-
const getWindowStorage = () => {
|
|
4758
|
-
if (typeof windowStorage === "undefined" || !windowStorage) {
|
|
4759
|
-
return null
|
|
4760
|
-
}
|
|
4761
|
-
return windowStorage
|
|
4762
|
-
}
|
|
4763
|
-
const frameContextKey = () => {
|
|
4764
|
-
const frameName = window.frameElement?.name || ""
|
|
4765
|
-
if (!frameName) {
|
|
4766
|
-
return ""
|
|
4767
|
-
}
|
|
4768
|
-
const datasetSrc = window.frameElement?.dataset?.src
|
|
4769
|
-
if (typeof datasetSrc === 'string' && datasetSrc.length > 0) {
|
|
4770
|
-
return `${frameName}:${datasetSrc}`
|
|
4771
|
-
}
|
|
4772
|
-
try {
|
|
4773
|
-
const srcUrl = window.frameElement?.src ? new URL(window.frameElement.src, window.location.origin) : null
|
|
4774
|
-
if (srcUrl) {
|
|
4775
|
-
return `${frameName}:${srcUrl.pathname}${srcUrl.search}${srcUrl.hash}`
|
|
4776
|
-
}
|
|
4777
|
-
} catch (_) {}
|
|
4778
|
-
return frameName
|
|
4779
|
-
}
|
|
4780
|
-
const selectionStorageKey = () => {
|
|
4781
|
-
const base = frameContextKey()
|
|
4782
|
-
if (!base) {
|
|
4783
|
-
return ""
|
|
4784
|
-
}
|
|
4785
|
-
return `${base}:selector`
|
|
4786
|
-
}
|
|
4787
|
-
const selectionUrlStorageKey = () => {
|
|
4788
|
-
const base = frameContextKey()
|
|
4789
|
-
if (!base) {
|
|
4790
|
-
return ""
|
|
4791
|
-
}
|
|
4792
|
-
return `${base}:url`
|
|
4793
|
-
}
|
|
4794
|
-
const SELECTION_SELECTOR_ATTRS = [
|
|
4795
|
-
'data-index',
|
|
4796
|
-
'target',
|
|
4797
|
-
'href',
|
|
4798
|
-
'data-target-full',
|
|
4799
|
-
'data-shell',
|
|
4800
|
-
'data-script',
|
|
4801
|
-
'data-action',
|
|
4802
|
-
'data-run',
|
|
4803
|
-
'data-command',
|
|
4804
|
-
'data-filepath'
|
|
4805
|
-
]
|
|
4806
|
-
const buildFrameLinkSelector = (node) => {
|
|
4807
|
-
if (!node || !node.classList || !node.classList.contains('frame-link')) {
|
|
4808
|
-
return null
|
|
4809
|
-
}
|
|
4810
|
-
for (const attr of SELECTION_SELECTOR_ATTRS) {
|
|
4811
|
-
const raw = node.getAttribute(attr)
|
|
4812
|
-
if (typeof raw === 'string' && raw.length > 0) {
|
|
4813
|
-
return `.frame-link[${attr}='${escapeTargetSelector(raw)}']`
|
|
4814
|
-
}
|
|
4815
|
-
}
|
|
4816
|
-
return null
|
|
4817
|
-
}
|
|
4818
|
-
const findLinkByAbsoluteHref = (href) => {
|
|
4819
|
-
if (!href) {
|
|
4820
|
-
return null
|
|
4821
|
-
}
|
|
4822
|
-
return Array.from(document.querySelectorAll('.frame-link')).find((el) => {
|
|
4823
|
-
try {
|
|
4824
|
-
return el.href === href
|
|
4825
|
-
} catch (_) {
|
|
4826
|
-
return false
|
|
4827
|
-
}
|
|
4828
|
-
}) || null
|
|
4829
|
-
}
|
|
4830
|
-
const persistFrameLinkSelection = (node) => {
|
|
4831
|
-
const storage = getWindowStorage()
|
|
4832
|
-
if (!storage || !node || !node.classList || !node.classList.contains('frame-link')) {
|
|
4833
|
-
return
|
|
4834
|
-
}
|
|
4835
|
-
const payload = {
|
|
4836
|
-
selector: buildFrameLinkSelector(node),
|
|
4837
|
-
hrefAttr: node.getAttribute('href') || null,
|
|
4838
|
-
href: (() => {
|
|
4839
|
-
try {
|
|
4840
|
-
return node.href || null
|
|
4841
|
-
} catch (_) {
|
|
4842
|
-
return null
|
|
4843
|
-
}
|
|
4844
|
-
})(),
|
|
4845
|
-
target: node.getAttribute('target') || null,
|
|
4846
|
-
dataIndex: node.getAttribute('data-index') || null,
|
|
4847
|
-
pagePath: (() => {
|
|
4848
|
-
try {
|
|
4849
|
-
return window.location?.pathname || null
|
|
4850
|
-
} catch (_) {
|
|
3381
|
+
})(),
|
|
3382
|
+
target: node.getAttribute('target') || null,
|
|
3383
|
+
pagePath: (() => {
|
|
3384
|
+
try {
|
|
3385
|
+
return window.location?.pathname || null
|
|
3386
|
+
} catch (_) {
|
|
4851
3387
|
return null
|
|
4852
3388
|
}
|
|
4853
3389
|
})()
|
|
@@ -4900,6 +3436,12 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4900
3436
|
if (typeof payload === 'string') {
|
|
4901
3437
|
payload = { selector: payload }
|
|
4902
3438
|
}
|
|
3439
|
+
if (payload && typeof payload.selector === 'string' && /\[data-index=/.test(payload.selector)) {
|
|
3440
|
+
payload.selector = null
|
|
3441
|
+
}
|
|
3442
|
+
if (payload && Object.prototype.hasOwnProperty.call(payload, 'dataIndex')) {
|
|
3443
|
+
delete payload.dataIndex
|
|
3444
|
+
}
|
|
4903
3445
|
const trySelector = (selector) => {
|
|
4904
3446
|
if (!selector || typeof selector !== 'string') {
|
|
4905
3447
|
return null
|
|
@@ -4917,9 +3459,6 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4917
3459
|
if (!node && payload.target) {
|
|
4918
3460
|
node = trySelector(`.frame-link[target='${escapeTargetSelector(payload.target)}']`)
|
|
4919
3461
|
}
|
|
4920
|
-
if (!node && payload.dataIndex) {
|
|
4921
|
-
node = trySelector(`.frame-link[data-index='${escapeTargetSelector(payload.dataIndex)}']`)
|
|
4922
|
-
}
|
|
4923
3462
|
if (!node && payload.href) {
|
|
4924
3463
|
node = findLinkByAbsoluteHref(payload.href)
|
|
4925
3464
|
}
|
|
@@ -5682,17 +4221,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
5682
4221
|
target = preselected
|
|
5683
4222
|
}
|
|
5684
4223
|
|
|
5685
|
-
if (
|
|
5686
|
-
const defaultSelection = getDefaultSelection()
|
|
5687
|
-
if (defaultSelection) {
|
|
5688
|
-
target = defaultSelection
|
|
5689
|
-
} else if (skipPersistedSelection) {
|
|
5690
|
-
scheduleSelectionRetry()
|
|
5691
|
-
return
|
|
5692
|
-
}
|
|
5693
|
-
}
|
|
5694
|
-
|
|
5695
|
-
<% if (type === "run" && env.PINOKIO_SCRIPT_DEFAULT && env.PINOKIO_SCRIPT_DEFAULT.toString().toLowerCase() === "true") { %>
|
|
4224
|
+
<% if (autoselect && env.PINOKIO_SCRIPT_DEFAULT && env.PINOKIO_SCRIPT_DEFAULT.toString().toLowerCase() === "true") { %>
|
|
5696
4225
|
if (!target && !hasPersistedSelection) {
|
|
5697
4226
|
const defaultSelection = getDefaultSelection()
|
|
5698
4227
|
if (defaultSelection && defaultSelection.href) {
|
|
@@ -5727,10 +4256,6 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
5727
4256
|
target = preselected
|
|
5728
4257
|
}
|
|
5729
4258
|
|
|
5730
|
-
if (!target) {
|
|
5731
|
-
target = document.querySelector(".frame-link")
|
|
5732
|
-
}
|
|
5733
|
-
|
|
5734
4259
|
if (!target) {
|
|
5735
4260
|
document.querySelector(".container").classList.remove("active")
|
|
5736
4261
|
document.querySelector("aside").classList.add("active")
|
|
@@ -6033,7 +4558,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
6033
4558
|
item.href = url
|
|
6034
4559
|
item.setAttribute("data-index", index)
|
|
6035
4560
|
item.className = "btn header-item frame-link"
|
|
6036
|
-
item.innerHTML = `<div class='tab'><i class="fa-solid fa-link"></i><div class='display'>${url}</div><div class='flexible'></div><button class='btn2 del'><i class="fa-solid fa-
|
|
4561
|
+
item.innerHTML = `<div class='tab'><i class="fa-solid fa-link"></i><div class='display'>${url}</div><div class='flexible'></div><button class='btn2 del'><i class="fa-solid fa-xmark"></i></button></div>`
|
|
6037
4562
|
|
|
6038
4563
|
document.querySelector(".temp-menu").appendChild(item)
|
|
6039
4564
|
|
|
@@ -6492,29 +5017,6 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
6492
5017
|
if (target) {
|
|
6493
5018
|
e.preventDefault()
|
|
6494
5019
|
e.stopPropagation()
|
|
6495
|
-
if (typeof e.stopImmediatePropagation === "function") {
|
|
6496
|
-
e.stopImmediatePropagation()
|
|
6497
|
-
}
|
|
6498
|
-
const parentLink = target.closest(".frame-link")
|
|
6499
|
-
if (parentLink) {
|
|
6500
|
-
let selectorCandidate = ""
|
|
6501
|
-
const targetAttr = parentLink.getAttribute("target")
|
|
6502
|
-
if (targetAttr) {
|
|
6503
|
-
const escapedTarget = escapeTargetSelector(targetAttr)
|
|
6504
|
-
if (escapedTarget) {
|
|
6505
|
-
selectorCandidate = `.frame-link[target="${escapedTarget}"]`
|
|
6506
|
-
}
|
|
6507
|
-
}
|
|
6508
|
-
if (!selectorCandidate && parentLink.href) {
|
|
6509
|
-
const escapedHref = escapeTargetSelector(parentLink.getAttribute("href"))
|
|
6510
|
-
if (escapedHref) {
|
|
6511
|
-
selectorCandidate = `.frame-link[href="${escapedHref}"]`
|
|
6512
|
-
}
|
|
6513
|
-
}
|
|
6514
|
-
if (selectorCandidate) {
|
|
6515
|
-
global_selector = selectorCandidate
|
|
6516
|
-
}
|
|
6517
|
-
}
|
|
6518
5020
|
let shell = target.closest("[data-shell]")
|
|
6519
5021
|
if (shell) {
|
|
6520
5022
|
let shell_id = shell.getAttribute("data-shell")
|
|
@@ -7027,55 +5529,6 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
7027
5529
|
refresh_du()
|
|
7028
5530
|
refresh_du("logs")
|
|
7029
5531
|
renderSelection({ force: true })
|
|
7030
|
-
<% if (type !== 'run') { %>
|
|
7031
|
-
/*
|
|
7032
|
-
fetch("<%=repos%>").then((res) => {
|
|
7033
|
-
return res.text()
|
|
7034
|
-
}).then((repos) => {
|
|
7035
|
-
if (document.querySelector("#git-repos")) {
|
|
7036
|
-
document.querySelector("#git-repos").innerHTML = repos
|
|
7037
|
-
}
|
|
7038
|
-
})
|
|
7039
|
-
refresh()
|
|
7040
|
-
*/
|
|
7041
|
-
<% } %>
|
|
7042
|
-
<% if (plugin_menu) { %>
|
|
7043
|
-
// document.querySelector(".dynamic .reveal").click()
|
|
7044
|
-
<% } %>
|
|
7045
|
-
/*
|
|
7046
|
-
document.addEventListener("keydown", (e) => {
|
|
7047
|
-
let size = document.querySelectorAll(".selectable").length
|
|
7048
|
-
e = e || window.event;
|
|
7049
|
-
if (e.key === "ArrowUp") {
|
|
7050
|
-
if (cursorIndex > 0) {
|
|
7051
|
-
cursorIndex--;
|
|
7052
|
-
} else {
|
|
7053
|
-
cursorIndex = size-1;
|
|
7054
|
-
}
|
|
7055
|
-
//renderCursor()
|
|
7056
|
-
//let cursor = document.querySelector(".selectable.cursor")
|
|
7057
|
-
//cursor.scrollIntoView(false)
|
|
7058
|
-
//
|
|
7059
|
-
} else if (e.key === "ArrowDown") {
|
|
7060
|
-
if (cursorIndex < size-1) {
|
|
7061
|
-
cursorIndex++;
|
|
7062
|
-
} else {
|
|
7063
|
-
cursorIndex = 0;
|
|
7064
|
-
}
|
|
7065
|
-
//renderCursor()
|
|
7066
|
-
//let cursor = document.querySelector(".selectable.cursor")
|
|
7067
|
-
//cursor.scrollIntoView(false)
|
|
7068
|
-
} else if (e.key === "Enter") {
|
|
7069
|
-
//let selected = document.querySelector(".line.selected:not(.hidden) .btns a.selected")
|
|
7070
|
-
let cursor = document.querySelector(".selectable.cursor")
|
|
7071
|
-
if (cursor) {
|
|
7072
|
-
e.preventDefault()
|
|
7073
|
-
e.stopPropagation()
|
|
7074
|
-
cursor.click()
|
|
7075
|
-
}
|
|
7076
|
-
}
|
|
7077
|
-
});
|
|
7078
|
-
*/
|
|
7079
5532
|
<% if (type === "browse" || type === "files") { %>
|
|
7080
5533
|
const repoStatusCache = new Map()
|
|
7081
5534
|
let lastRepoList = []
|