pinokiod 3.97.0 → 3.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/public/layout.js +1 -1
- package/server/public/style.css +2 -3
- package/server/views/app.ejs +954 -21
- package/server/views/d.ejs +1 -1
- package/server/views/review.ejs +0 -1
package/package.json
CHANGED
package/server/public/layout.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
const HOST_ORIGIN = window.location.origin;
|
|
24
24
|
const STORAGE_PREFIX = 'pinokio:layout:';
|
|
25
25
|
const MIN_PANEL_SIZE = 120;
|
|
26
|
-
const GUTTER_SIZE =
|
|
26
|
+
const GUTTER_SIZE = 4;
|
|
27
27
|
|
|
28
28
|
const state = {
|
|
29
29
|
sessionId: typeof parsedConfig.sessionId === 'string' && parsedConfig.sessionId.trim() ? parsedConfig.sessionId.trim() : null,
|
package/server/public/style.css
CHANGED
|
@@ -164,7 +164,6 @@ aside .tab.submenu {
|
|
|
164
164
|
#genlog, #downloadlogs {
|
|
165
165
|
cursor: pointer;
|
|
166
166
|
}
|
|
167
|
-
|
|
168
167
|
.url-bar {
|
|
169
168
|
font-family: verdana;
|
|
170
169
|
font-size: 12px;
|
|
@@ -291,11 +290,11 @@ body.dark .navheader2 .btn {
|
|
|
291
290
|
/*
|
|
292
291
|
padding: 10px 0;
|
|
293
292
|
*/
|
|
294
|
-
padding: 0 0
|
|
293
|
+
padding: 0 0 4px;
|
|
295
294
|
/*
|
|
296
295
|
background: white;
|
|
297
296
|
*/
|
|
298
|
-
background:
|
|
297
|
+
background: #F9F9FB !important;
|
|
299
298
|
}
|
|
300
299
|
.navheader {
|
|
301
300
|
backdrop-filter: blur(16px);
|
package/server/views/app.ejs
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
<link href="/electron.css" rel="stylesheet"/>
|
|
19
19
|
<% } %>
|
|
20
20
|
<style>
|
|
21
|
+
body.dark #devtab {
|
|
22
|
+
border: none;
|
|
23
|
+
}
|
|
21
24
|
#devtab {
|
|
22
25
|
align-items: center;
|
|
23
26
|
justify-content: center;
|
|
@@ -31,6 +34,7 @@
|
|
|
31
34
|
background: none;
|
|
32
35
|
border-bottom-left-radius: 0;
|
|
33
36
|
border-bottom-right-radius: 0;
|
|
37
|
+
border-bottom: none;
|
|
34
38
|
}
|
|
35
39
|
#devtab .loader {
|
|
36
40
|
padding: 10px;
|
|
@@ -141,10 +145,12 @@ body.dark .appcanvas {
|
|
|
141
145
|
*/
|
|
142
146
|
body.dark .appcanvas_filler {
|
|
143
147
|
background: rgba(255,255,255,0.2) !important;
|
|
148
|
+
border: none;
|
|
144
149
|
}
|
|
145
150
|
.appcanvas_filler {
|
|
146
|
-
height:
|
|
147
|
-
background:
|
|
151
|
+
height: 4px;
|
|
152
|
+
background: #F9F9FB !important;
|
|
153
|
+
border-top: 1px solid var(--pinokio-sidebar-tabbar-border);
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
.appcanvas > .container {
|
|
@@ -379,13 +385,17 @@ body.dark .appcanvas > aside .m.menu .nested-menu > .submenu .frame-link:hover {
|
|
|
379
385
|
color: var(--pinokio-sidebar-tab-active-color);
|
|
380
386
|
}
|
|
381
387
|
|
|
388
|
+
body.dark .appcanvas > aside .header-item.selected {
|
|
389
|
+
border: none;
|
|
390
|
+
}
|
|
382
391
|
.appcanvas > aside .header-item.selected {
|
|
383
|
-
|
|
392
|
+
/*
|
|
393
|
+
border-color: var(--pinokio-sidebar-tab-active-bg);
|
|
394
|
+
*/
|
|
384
395
|
color: var(--pinokio-sidebar-tab-active-color);
|
|
385
|
-
/*
|
|
386
396
|
border-color: var(--sidebar-tab-outline);
|
|
387
|
-
*/
|
|
388
397
|
box-shadow: 0 4px 12px var(--pinokio-sidebar-tab-shadow);
|
|
398
|
+
background: #F9F9FB !important;
|
|
389
399
|
border-bottom: none;
|
|
390
400
|
z-index: 1;
|
|
391
401
|
}
|
|
@@ -685,11 +695,8 @@ body.dark .header-item.cursor {
|
|
|
685
695
|
cursor: pointer;
|
|
686
696
|
}
|
|
687
697
|
body .frame-link.selected {
|
|
688
|
-
background:
|
|
689
|
-
|
|
690
|
-
background: rgba(127, 91, 243, 0.9) !important;
|
|
691
|
-
color: white !important;
|
|
692
|
-
*/
|
|
698
|
+
background: #F9F9FB !important;
|
|
699
|
+
border: 1px solid rgba(203, 213, 225, 0.7);
|
|
693
700
|
}
|
|
694
701
|
.frame-link.selected .del {
|
|
695
702
|
color: white;
|
|
@@ -791,6 +798,94 @@ body.dark .grid-btns .btn2 {
|
|
|
791
798
|
flex: 1 1 auto;
|
|
792
799
|
min-width: 0;
|
|
793
800
|
}
|
|
801
|
+
.tab-link-popover {
|
|
802
|
+
position: fixed;
|
|
803
|
+
display: none;
|
|
804
|
+
flex-direction: column;
|
|
805
|
+
gap: 4px;
|
|
806
|
+
padding: 8px 0;
|
|
807
|
+
border-radius: 10px;
|
|
808
|
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
809
|
+
background: rgba(255, 255, 255, 0.97);
|
|
810
|
+
box-shadow: 0 12px 32px -8px rgba(15, 23, 42, 0.25);
|
|
811
|
+
z-index: 9999;
|
|
812
|
+
min-width: 240px;
|
|
813
|
+
max-width: 420px;
|
|
814
|
+
backdrop-filter: blur(6px);
|
|
815
|
+
}
|
|
816
|
+
body.dark .tab-link-popover {
|
|
817
|
+
border-color: rgba(255, 255, 255, 0.08);
|
|
818
|
+
background: rgba(17, 24, 39, 0.95);
|
|
819
|
+
box-shadow: 0 12px 36px -12px rgba(15, 23, 42, 0.55);
|
|
820
|
+
}
|
|
821
|
+
.tab-link-popover.visible {
|
|
822
|
+
display: flex;
|
|
823
|
+
}
|
|
824
|
+
.tab-link-popover .tab-link-popover-header {
|
|
825
|
+
display: flex;
|
|
826
|
+
align-items: center;
|
|
827
|
+
gap: 8px;
|
|
828
|
+
padding: 10px 14px 6px;
|
|
829
|
+
font-size: 11px;
|
|
830
|
+
font-weight: 600;
|
|
831
|
+
letter-spacing: 0.05em;
|
|
832
|
+
text-transform: uppercase;
|
|
833
|
+
color: rgba(15, 23, 42, 0.55);
|
|
834
|
+
}
|
|
835
|
+
.tab-link-popover .tab-link-popover-header i {
|
|
836
|
+
font-size: 12px;
|
|
837
|
+
}
|
|
838
|
+
body.dark .tab-link-popover .tab-link-popover-header {
|
|
839
|
+
color: rgba(226, 232, 240, 0.7);
|
|
840
|
+
}
|
|
841
|
+
.tab-link-popover .tab-link-popover-item {
|
|
842
|
+
width: 100%;
|
|
843
|
+
border: none;
|
|
844
|
+
margin: 0;
|
|
845
|
+
padding: 8px 14px;
|
|
846
|
+
display: flex;
|
|
847
|
+
flex-direction: column;
|
|
848
|
+
gap: 2px;
|
|
849
|
+
text-align: left;
|
|
850
|
+
font: inherit;
|
|
851
|
+
color: inherit;
|
|
852
|
+
background: transparent;
|
|
853
|
+
cursor: pointer;
|
|
854
|
+
}
|
|
855
|
+
.tab-link-popover .tab-link-popover-item:hover,
|
|
856
|
+
.tab-link-popover .tab-link-popover-item:focus-visible {
|
|
857
|
+
background: rgba(15, 23, 42, 0.06);
|
|
858
|
+
outline: none;
|
|
859
|
+
}
|
|
860
|
+
body.dark .tab-link-popover .tab-link-popover-item:hover,
|
|
861
|
+
body.dark .tab-link-popover .tab-link-popover-item:focus-visible {
|
|
862
|
+
background: rgba(148, 163, 184, 0.12);
|
|
863
|
+
}
|
|
864
|
+
.tab-link-popover .tab-link-popover-item .label {
|
|
865
|
+
font-size: 11px;
|
|
866
|
+
font-weight: 600;
|
|
867
|
+
letter-spacing: 0.04em;
|
|
868
|
+
text-transform: uppercase;
|
|
869
|
+
color: rgba(15, 23, 42, 0.55);
|
|
870
|
+
}
|
|
871
|
+
body.dark .tab-link-popover .tab-link-popover-item .label {
|
|
872
|
+
color: rgba(226, 232, 240, 0.65);
|
|
873
|
+
}
|
|
874
|
+
.tab-link-popover .tab-link-popover-item .value {
|
|
875
|
+
font-size: 13px;
|
|
876
|
+
line-height: 1.35;
|
|
877
|
+
word-break: break-word;
|
|
878
|
+
color: rgba(15, 23, 42, 0.85);
|
|
879
|
+
}
|
|
880
|
+
body.dark .tab-link-popover .tab-link-popover-item .value {
|
|
881
|
+
color: rgba(226, 232, 240, 0.9);
|
|
882
|
+
}
|
|
883
|
+
.tab-link-popover .tab-link-popover-item .value .muted {
|
|
884
|
+
color: rgba(15, 23, 42, 0.55);
|
|
885
|
+
}
|
|
886
|
+
body.dark .tab-link-popover .tab-link-popover-item .value .muted {
|
|
887
|
+
color: rgba(226, 232, 240, 0.65);
|
|
888
|
+
}
|
|
794
889
|
.tab.has-preview .tab-preview {
|
|
795
890
|
color: rgba(0, 0, 0, 0.6);
|
|
796
891
|
min-width: 0;
|
|
@@ -1181,11 +1276,13 @@ body.dark #fs-status {
|
|
|
1181
1276
|
/*
|
|
1182
1277
|
border-bottom: 1px solid rgba(255,255,255,0.04);
|
|
1183
1278
|
*/
|
|
1279
|
+
border: none;
|
|
1184
1280
|
background: rgba(255,255,255,0.2) !important;
|
|
1185
1281
|
}
|
|
1186
1282
|
#fs-status {
|
|
1187
1283
|
padding: 5px;
|
|
1188
|
-
background:
|
|
1284
|
+
background: #F9F9FB !important;
|
|
1285
|
+
border-top: 1px solid rgba(203, 213, 225, 0.7);
|
|
1189
1286
|
gap: 4px;
|
|
1190
1287
|
box-sizing: border-box;
|
|
1191
1288
|
/*
|
|
@@ -2413,7 +2510,6 @@ body.dark {
|
|
|
2413
2510
|
<a class="btn2 <%=type === 'browse' ? 'selected' : ''%>" href="<%=dev_tab%>"><div><i class="fa-solid fa-code"></i></div><div class='caption'>Dev</div></a>
|
|
2414
2511
|
<a class="btn2 <%=type === 'run' ? 'selected' : ''%>" href="<%=run_tab%>"><div><i class="fa-solid fa-circle-play"></i></div><div class='caption'>Run</div></a>
|
|
2415
2512
|
</div>
|
|
2416
|
-
<button class='btn2' id='popout-link' data-tippy-content="open in a web browser"><i class="fa-solid fa-arrow-up-right-from-square"></i></button>
|
|
2417
2513
|
<a class='btn2' href="/columns" data-tippy-content="split into 2 columns">
|
|
2418
2514
|
<div><i class="fa-solid fa-table-columns"></i></div>
|
|
2419
2515
|
</a>
|
|
@@ -2722,6 +2818,845 @@ body.dark {
|
|
|
2722
2818
|
const updatedClass = "tab-updated"
|
|
2723
2819
|
const tabStateStore = new Map()
|
|
2724
2820
|
const TAB_STATE_STORAGE_KEY = "tab-state-store"
|
|
2821
|
+
const TAB_LINK_POPOVER_ID = "tab-link-popover"
|
|
2822
|
+
let tabLinkPopoverEl = null
|
|
2823
|
+
let tabLinkActiveLink = null
|
|
2824
|
+
let tabLinkPendingLink = null
|
|
2825
|
+
let tabLinkHideTimer = null
|
|
2826
|
+
let tabLinkLocalInfoPromise = null
|
|
2827
|
+
let tabLinkLocalInfoExpiry = 0
|
|
2828
|
+
let tabLinkRouterInfoPromise = null
|
|
2829
|
+
let tabLinkRouterInfoExpiry = 0
|
|
2830
|
+
|
|
2831
|
+
const ensureTabLinkPopoverEl = () => {
|
|
2832
|
+
if (!tabLinkPopoverEl) {
|
|
2833
|
+
tabLinkPopoverEl = document.createElement("div")
|
|
2834
|
+
tabLinkPopoverEl.id = TAB_LINK_POPOVER_ID
|
|
2835
|
+
tabLinkPopoverEl.className = "tab-link-popover"
|
|
2836
|
+
tabLinkPopoverEl.addEventListener("mouseenter", () => {
|
|
2837
|
+
if (tabLinkHideTimer) {
|
|
2838
|
+
clearTimeout(tabLinkHideTimer)
|
|
2839
|
+
tabLinkHideTimer = null
|
|
2840
|
+
}
|
|
2841
|
+
})
|
|
2842
|
+
tabLinkPopoverEl.addEventListener("mouseleave", () => {
|
|
2843
|
+
hideTabLinkPopover({ immediate: true })
|
|
2844
|
+
})
|
|
2845
|
+
tabLinkPopoverEl.addEventListener("click", (event) => {
|
|
2846
|
+
const item = event.target.closest(".tab-link-popover-item")
|
|
2847
|
+
if (!item) {
|
|
2848
|
+
return
|
|
2849
|
+
}
|
|
2850
|
+
event.preventDefault()
|
|
2851
|
+
event.stopPropagation()
|
|
2852
|
+
const url = item.getAttribute("data-url")
|
|
2853
|
+
if (url) {
|
|
2854
|
+
window.open(url, "_blank", "noopener")
|
|
2855
|
+
}
|
|
2856
|
+
hideTabLinkPopover({ immediate: true })
|
|
2857
|
+
})
|
|
2858
|
+
document.body.appendChild(tabLinkPopoverEl)
|
|
2859
|
+
}
|
|
2860
|
+
return tabLinkPopoverEl
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
const canonicalizeUrl = (value) => {
|
|
2864
|
+
try {
|
|
2865
|
+
const parsed = new URL(value, location.origin)
|
|
2866
|
+
if (!parsed.protocol) {
|
|
2867
|
+
return value
|
|
2868
|
+
}
|
|
2869
|
+
const protocol = parsed.protocol.toLowerCase()
|
|
2870
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
2871
|
+
return value
|
|
2872
|
+
}
|
|
2873
|
+
const hostname = parsed.hostname.toLowerCase()
|
|
2874
|
+
const port = parsed.port ? `:${parsed.port}` : ""
|
|
2875
|
+
let pathname = parsed.pathname || "/"
|
|
2876
|
+
if (pathname !== "/") {
|
|
2877
|
+
pathname = pathname.replace(/\/+/g, "/")
|
|
2878
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
2879
|
+
pathname = pathname.slice(0, -1)
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
const search = parsed.search || ""
|
|
2883
|
+
return `${protocol}//${hostname}${port}${pathname}${search}`
|
|
2884
|
+
} catch (_) {
|
|
2885
|
+
return value
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
const formatDisplayUrl = (value) => {
|
|
2890
|
+
try {
|
|
2891
|
+
const parsed = new URL(value, location.origin)
|
|
2892
|
+
const host = parsed.host
|
|
2893
|
+
const pathname = parsed.pathname || "/"
|
|
2894
|
+
const search = parsed.search || ""
|
|
2895
|
+
return `${host}${pathname}${search}`
|
|
2896
|
+
} catch (_) {
|
|
2897
|
+
return value
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
const isHttpOrHttps = (value) => {
|
|
2902
|
+
try {
|
|
2903
|
+
const parsed = new URL(value, location.origin)
|
|
2904
|
+
const protocol = parsed.protocol.toLowerCase()
|
|
2905
|
+
return protocol === "http:" || protocol === "https:"
|
|
2906
|
+
} catch (_) {
|
|
2907
|
+
return false
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
const isHttpUrl = (value) => {
|
|
2912
|
+
try {
|
|
2913
|
+
const parsed = new URL(value, location.origin)
|
|
2914
|
+
return parsed.protocol.toLowerCase() === "http:"
|
|
2915
|
+
} catch (_) {
|
|
2916
|
+
return false
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
const isHttpsUrl = (value) => {
|
|
2921
|
+
try {
|
|
2922
|
+
const parsed = new URL(value, location.origin)
|
|
2923
|
+
return parsed.protocol.toLowerCase() === "https:"
|
|
2924
|
+
} catch (_) {
|
|
2925
|
+
return false
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
const collectUrlsFromLocal = (root) => {
|
|
2930
|
+
if (!root || typeof root !== "object") {
|
|
2931
|
+
return []
|
|
2932
|
+
}
|
|
2933
|
+
const queue = [root]
|
|
2934
|
+
const visited = new Set()
|
|
2935
|
+
const urls = new Set()
|
|
2936
|
+
while (queue.length > 0) {
|
|
2937
|
+
const current = queue.shift()
|
|
2938
|
+
if (!current || typeof current !== "object") {
|
|
2939
|
+
continue
|
|
2940
|
+
}
|
|
2941
|
+
if (visited.has(current)) {
|
|
2942
|
+
continue
|
|
2943
|
+
}
|
|
2944
|
+
visited.add(current)
|
|
2945
|
+
const values = Array.isArray(current) ? current : Object.values(current)
|
|
2946
|
+
for (const value of values) {
|
|
2947
|
+
if (typeof value === "string") {
|
|
2948
|
+
if (isHttpOrHttps(value)) {
|
|
2949
|
+
urls.add(value)
|
|
2950
|
+
}
|
|
2951
|
+
} else if (value && typeof value === "object") {
|
|
2952
|
+
queue.push(value)
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
return Array.from(urls)
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
const collectScriptKeys = (node) => {
|
|
2960
|
+
const keys = new Set()
|
|
2961
|
+
const scriptAttr = node.getAttribute("data-script")
|
|
2962
|
+
if (scriptAttr) {
|
|
2963
|
+
const decoded = decodeURIComponent(scriptAttr)
|
|
2964
|
+
if (decoded) {
|
|
2965
|
+
keys.add(decoded)
|
|
2966
|
+
const withoutQuery = decoded.split("?")[0]
|
|
2967
|
+
if (withoutQuery) {
|
|
2968
|
+
keys.add(withoutQuery)
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
const filepathAttr = node.getAttribute("data-filepath")
|
|
2973
|
+
if (filepathAttr) {
|
|
2974
|
+
keys.add(filepathAttr)
|
|
2975
|
+
}
|
|
2976
|
+
return Array.from(keys)
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
const ensureLocalMemory = async () => {
|
|
2980
|
+
const now = Date.now()
|
|
2981
|
+
if (!tabLinkLocalInfoPromise || now > tabLinkLocalInfoExpiry) {
|
|
2982
|
+
tabLinkLocalInfoPromise = fetch("/info/local", {
|
|
2983
|
+
method: "GET",
|
|
2984
|
+
headers: {
|
|
2985
|
+
"Accept": "application/json"
|
|
2986
|
+
}
|
|
2987
|
+
})
|
|
2988
|
+
.then((response) => {
|
|
2989
|
+
if (!response.ok) {
|
|
2990
|
+
throw new Error("Failed to load local info")
|
|
2991
|
+
}
|
|
2992
|
+
return response.json()
|
|
2993
|
+
})
|
|
2994
|
+
.catch(() => ({}))
|
|
2995
|
+
tabLinkLocalInfoExpiry = now + 3000
|
|
2996
|
+
}
|
|
2997
|
+
return tabLinkLocalInfoPromise
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
const normalizeHttpsTarget = (value) => {
|
|
3001
|
+
if (!value || typeof value !== "string") {
|
|
3002
|
+
return ""
|
|
3003
|
+
}
|
|
3004
|
+
let trimmed = value.trim()
|
|
3005
|
+
if (!trimmed) {
|
|
3006
|
+
return ""
|
|
3007
|
+
}
|
|
3008
|
+
if (!/^https?:\/\//i.test(trimmed)) {
|
|
3009
|
+
trimmed = `https://${trimmed}`
|
|
3010
|
+
} else {
|
|
3011
|
+
trimmed = trimmed.replace(/^http:/i, "https:")
|
|
3012
|
+
}
|
|
3013
|
+
try {
|
|
3014
|
+
const parsed = new URL(trimmed)
|
|
3015
|
+
if (!parsed.host) {
|
|
3016
|
+
return ""
|
|
3017
|
+
}
|
|
3018
|
+
let pathname = parsed.pathname || ""
|
|
3019
|
+
if (pathname === "/") {
|
|
3020
|
+
pathname = ""
|
|
3021
|
+
}
|
|
3022
|
+
const search = parsed.search || ""
|
|
3023
|
+
return `https://${parsed.host}${pathname}${search}`
|
|
3024
|
+
} catch (_) {
|
|
3025
|
+
return ""
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
const parseHostPort = (value) => {
|
|
3030
|
+
if (!value || typeof value !== "string") {
|
|
3031
|
+
return null
|
|
3032
|
+
}
|
|
3033
|
+
let trimmed = value.trim()
|
|
3034
|
+
if (!trimmed) {
|
|
3035
|
+
return null
|
|
3036
|
+
}
|
|
3037
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
3038
|
+
try {
|
|
3039
|
+
const parsed = new URL(trimmed)
|
|
3040
|
+
if (!parsed.hostname) {
|
|
3041
|
+
return null
|
|
3042
|
+
}
|
|
3043
|
+
const protocol = parsed.protocol.toLowerCase()
|
|
3044
|
+
let port = parsed.port
|
|
3045
|
+
if (!port) {
|
|
3046
|
+
if (protocol === "http:") {
|
|
3047
|
+
port = "80"
|
|
3048
|
+
} else if (protocol === "https:") {
|
|
3049
|
+
port = "443"
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
if (!port) {
|
|
3053
|
+
return null
|
|
3054
|
+
}
|
|
3055
|
+
return {
|
|
3056
|
+
host: parsed.hostname.toLowerCase(),
|
|
3057
|
+
port
|
|
3058
|
+
}
|
|
3059
|
+
} catch (_) {
|
|
3060
|
+
return null
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
const slashIndex = trimmed.indexOf("/")
|
|
3064
|
+
if (slashIndex >= 0) {
|
|
3065
|
+
trimmed = trimmed.slice(0, slashIndex)
|
|
3066
|
+
}
|
|
3067
|
+
const match = trimmed.match(/^\[?([^\]]+)\]?(?::([0-9]+))$/)
|
|
3068
|
+
if (!match) {
|
|
3069
|
+
return null
|
|
3070
|
+
}
|
|
3071
|
+
const host = match[1] ? match[1].toLowerCase() : ""
|
|
3072
|
+
const port = match[2] || ""
|
|
3073
|
+
if (!host || !port) {
|
|
3074
|
+
return null
|
|
3075
|
+
}
|
|
3076
|
+
return { host, port }
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
const ensureRouterInfoMapping = async () => {
|
|
3080
|
+
const now = Date.now()
|
|
3081
|
+
if (!tabLinkRouterInfoPromise || now > tabLinkRouterInfoExpiry) {
|
|
3082
|
+
tabLinkRouterInfoPromise = fetch("/info/system", {
|
|
3083
|
+
method: "GET",
|
|
3084
|
+
headers: {
|
|
3085
|
+
"Accept": "application/json"
|
|
3086
|
+
}
|
|
3087
|
+
})
|
|
3088
|
+
.then((response) => {
|
|
3089
|
+
if (!response.ok) {
|
|
3090
|
+
throw new Error("Failed to load system info")
|
|
3091
|
+
}
|
|
3092
|
+
return response.json()
|
|
3093
|
+
})
|
|
3094
|
+
.then((data) => {
|
|
3095
|
+
const processes = Array.isArray(data?.router_info) ? data.router_info : []
|
|
3096
|
+
const rewriteMapping = data?.rewrite_mapping && typeof data.rewrite_mapping === "object"
|
|
3097
|
+
? Object.values(data.rewrite_mapping)
|
|
3098
|
+
: []
|
|
3099
|
+
const portMap = new Map()
|
|
3100
|
+
const hostPortMap = new Map()
|
|
3101
|
+
const hostAliasPortMap = new Map()
|
|
3102
|
+
if (data?.router && typeof data.router === "object") {
|
|
3103
|
+
Object.entries(data.router).forEach(([dial, hosts]) => {
|
|
3104
|
+
const parsedDial = parseHostPort(dial)
|
|
3105
|
+
if (!parsedDial || !parsedDial.port) {
|
|
3106
|
+
return
|
|
3107
|
+
}
|
|
3108
|
+
if (!Array.isArray(hosts)) {
|
|
3109
|
+
return
|
|
3110
|
+
}
|
|
3111
|
+
hosts.forEach((host) => {
|
|
3112
|
+
if (typeof host !== "string") {
|
|
3113
|
+
return
|
|
3114
|
+
}
|
|
3115
|
+
const trimmed = host.trim().toLowerCase()
|
|
3116
|
+
if (!trimmed) {
|
|
3117
|
+
return
|
|
3118
|
+
}
|
|
3119
|
+
if (!hostAliasPortMap.has(trimmed)) {
|
|
3120
|
+
hostAliasPortMap.set(trimmed, new Set())
|
|
3121
|
+
}
|
|
3122
|
+
hostAliasPortMap.get(trimmed).add(parsedDial.port)
|
|
3123
|
+
})
|
|
3124
|
+
})
|
|
3125
|
+
}
|
|
3126
|
+
const localAliases = ["127.0.0.1", "localhost", "0.0.0.0", "::1", "[::1]"]
|
|
3127
|
+
|
|
3128
|
+
const addHttpMapping = (host, port, httpsSet) => {
|
|
3129
|
+
if (!host || !port || !httpsSet || httpsSet.size === 0) {
|
|
3130
|
+
return
|
|
3131
|
+
}
|
|
3132
|
+
const hostLower = host.toLowerCase()
|
|
3133
|
+
const keys = new Set([`${hostLower}:${port}`])
|
|
3134
|
+
if (localAliases.includes(hostLower)) {
|
|
3135
|
+
localAliases.forEach((alias) => keys.add(`${alias}:${port}`))
|
|
3136
|
+
}
|
|
3137
|
+
keys.forEach((key) => {
|
|
3138
|
+
if (!hostPortMap.has(key)) {
|
|
3139
|
+
hostPortMap.set(key, new Set())
|
|
3140
|
+
}
|
|
3141
|
+
const set = hostPortMap.get(key)
|
|
3142
|
+
httpsSet.forEach((url) => set.add(url))
|
|
3143
|
+
})
|
|
3144
|
+
if (localAliases.includes(hostLower)) {
|
|
3145
|
+
if (!portMap.has(port)) {
|
|
3146
|
+
portMap.set(port, new Set())
|
|
3147
|
+
}
|
|
3148
|
+
const portSet = portMap.get(port)
|
|
3149
|
+
httpsSet.forEach((url) => portSet.add(url))
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
const gatherHttpsTargets = (value) => {
|
|
3154
|
+
const targets = new Set()
|
|
3155
|
+
const visit = (input) => {
|
|
3156
|
+
if (!input) {
|
|
3157
|
+
return
|
|
3158
|
+
}
|
|
3159
|
+
if (Array.isArray(input)) {
|
|
3160
|
+
input.forEach(visit)
|
|
3161
|
+
return
|
|
3162
|
+
}
|
|
3163
|
+
if (typeof input === "object") {
|
|
3164
|
+
Object.values(input).forEach(visit)
|
|
3165
|
+
return
|
|
3166
|
+
}
|
|
3167
|
+
if (typeof input !== "string") {
|
|
3168
|
+
return
|
|
3169
|
+
}
|
|
3170
|
+
const normalized = normalizeHttpsTarget(input)
|
|
3171
|
+
if (normalized) {
|
|
3172
|
+
targets.add(normalized)
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
visit(value)
|
|
3176
|
+
return targets
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
const collectHostPort = (value, hostPortCandidates, portCandidates) => {
|
|
3180
|
+
if (!value) {
|
|
3181
|
+
return
|
|
3182
|
+
}
|
|
3183
|
+
if (Array.isArray(value)) {
|
|
3184
|
+
value.forEach((item) => collectHostPort(item, hostPortCandidates, portCandidates))
|
|
3185
|
+
return
|
|
3186
|
+
}
|
|
3187
|
+
if (typeof value === "object") {
|
|
3188
|
+
Object.values(value).forEach((item) => {
|
|
3189
|
+
collectHostPort(item, hostPortCandidates, portCandidates)
|
|
3190
|
+
})
|
|
3191
|
+
return
|
|
3192
|
+
}
|
|
3193
|
+
if (typeof value !== "string") {
|
|
3194
|
+
return
|
|
3195
|
+
}
|
|
3196
|
+
const parsed = parseHostPort(value)
|
|
3197
|
+
let hostLower
|
|
3198
|
+
if (parsed && parsed.host && parsed.port) {
|
|
3199
|
+
hostLower = parsed.host.toLowerCase()
|
|
3200
|
+
hostPortCandidates.add(`${hostLower}:${parsed.port}`)
|
|
3201
|
+
if (localAliases.includes(hostLower)) {
|
|
3202
|
+
portCandidates.add(parsed.port)
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
const rawHost = value.replace(/^https?:\/\//i, "").split("/")[0].toLowerCase()
|
|
3206
|
+
const aliasPorts = hostAliasPortMap.get(rawHost)
|
|
3207
|
+
if (aliasPorts && aliasPorts.size > 0) {
|
|
3208
|
+
aliasPorts.forEach((aliasPort) => {
|
|
3209
|
+
hostPortCandidates.add(`${rawHost}:${aliasPort}`)
|
|
3210
|
+
if (localAliases.includes(rawHost)) {
|
|
3211
|
+
portCandidates.add(aliasPort)
|
|
3212
|
+
}
|
|
3213
|
+
})
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
const collectPort = (value, portCandidates) => {
|
|
3218
|
+
if (value === null || value === undefined || value === "") {
|
|
3219
|
+
return
|
|
3220
|
+
}
|
|
3221
|
+
if (Array.isArray(value)) {
|
|
3222
|
+
value.forEach((item) => collectPort(item, portCandidates))
|
|
3223
|
+
return
|
|
3224
|
+
}
|
|
3225
|
+
const port = `${value}`.trim()
|
|
3226
|
+
if (port && /^[0-9]+$/.test(port)) {
|
|
3227
|
+
portCandidates.add(port)
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
const registerEntry = (entry) => {
|
|
3232
|
+
if (!entry || typeof entry !== "object") {
|
|
3233
|
+
return
|
|
3234
|
+
}
|
|
3235
|
+
const httpsTargets = new Set()
|
|
3236
|
+
const mergeTargets = (targetValue) => {
|
|
3237
|
+
const targets = gatherHttpsTargets(targetValue)
|
|
3238
|
+
targets.forEach((url) => httpsTargets.add(url))
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
mergeTargets(entry.external_router)
|
|
3242
|
+
mergeTargets(entry.external_domain)
|
|
3243
|
+
mergeTargets(entry.https_href)
|
|
3244
|
+
mergeTargets(entry.app_href)
|
|
3245
|
+
mergeTargets(entry.external_ip)
|
|
3246
|
+
mergeTargets(entry.internal_router)
|
|
3247
|
+
mergeTargets(entry.match)
|
|
3248
|
+
mergeTargets(entry.host)
|
|
3249
|
+
mergeTargets(entry.hosts)
|
|
3250
|
+
|
|
3251
|
+
if (httpsTargets.size === 0) {
|
|
3252
|
+
return
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
const hostPortCandidates = new Set()
|
|
3256
|
+
const portCandidates = new Set()
|
|
3257
|
+
|
|
3258
|
+
collectHostPort(entry.external_ip, hostPortCandidates, portCandidates)
|
|
3259
|
+
collectHostPort(entry.internal_ip, hostPortCandidates, portCandidates)
|
|
3260
|
+
collectHostPort(entry.ip, hostPortCandidates, portCandidates)
|
|
3261
|
+
collectHostPort(entry.dial, hostPortCandidates, portCandidates)
|
|
3262
|
+
collectHostPort(entry.match, hostPortCandidates, portCandidates)
|
|
3263
|
+
collectHostPort(entry.target, hostPortCandidates, portCandidates)
|
|
3264
|
+
collectHostPort(entry.forward, hostPortCandidates, portCandidates)
|
|
3265
|
+
collectHostPort(entry.internal_router, hostPortCandidates, portCandidates)
|
|
3266
|
+
collectHostPort(entry.external_router, hostPortCandidates, portCandidates)
|
|
3267
|
+
|
|
3268
|
+
collectPort(entry.port, portCandidates)
|
|
3269
|
+
collectPort(entry.internal_port, portCandidates)
|
|
3270
|
+
collectPort(entry.external_port, portCandidates)
|
|
3271
|
+
|
|
3272
|
+
if (hostPortCandidates.size === 0 && portCandidates.size === 0) {
|
|
3273
|
+
httpsTargets.forEach((target) => {
|
|
3274
|
+
collectHostPort(target, hostPortCandidates, portCandidates)
|
|
3275
|
+
})
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
if (hostPortCandidates.size === 0 && portCandidates.size === 0) {
|
|
3279
|
+
return
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
hostPortCandidates.forEach((key) => {
|
|
3283
|
+
const parsed = parseHostPort(key)
|
|
3284
|
+
if (parsed) {
|
|
3285
|
+
addHttpMapping(parsed.host, parsed.port, httpsTargets)
|
|
3286
|
+
}
|
|
3287
|
+
})
|
|
3288
|
+
|
|
3289
|
+
portCandidates.forEach((port) => {
|
|
3290
|
+
localAliases.forEach((host) => {
|
|
3291
|
+
addHttpMapping(host, port, httpsTargets)
|
|
3292
|
+
})
|
|
3293
|
+
})
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
const visited = new WeakSet()
|
|
3297
|
+
const traverseNode = (node) => {
|
|
3298
|
+
if (!node) {
|
|
3299
|
+
return
|
|
3300
|
+
}
|
|
3301
|
+
if (Array.isArray(node)) {
|
|
3302
|
+
node.forEach(traverseNode)
|
|
3303
|
+
return
|
|
3304
|
+
}
|
|
3305
|
+
if (typeof node !== "object") {
|
|
3306
|
+
return
|
|
3307
|
+
}
|
|
3308
|
+
if (visited.has(node)) {
|
|
3309
|
+
return
|
|
3310
|
+
}
|
|
3311
|
+
visited.add(node)
|
|
3312
|
+
registerEntry(node)
|
|
3313
|
+
Object.values(node).forEach((value) => {
|
|
3314
|
+
if (value && typeof value === "object") {
|
|
3315
|
+
traverseNode(value)
|
|
3316
|
+
}
|
|
3317
|
+
})
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
processes.forEach(traverseNode)
|
|
3321
|
+
rewriteMapping.forEach(traverseNode)
|
|
3322
|
+
|
|
3323
|
+
return {
|
|
3324
|
+
portMap,
|
|
3325
|
+
hostPortMap
|
|
3326
|
+
}
|
|
3327
|
+
})
|
|
3328
|
+
.catch(() => ({
|
|
3329
|
+
portMap: new Map(),
|
|
3330
|
+
hostPortMap: new Map()
|
|
3331
|
+
}))
|
|
3332
|
+
tabLinkRouterInfoExpiry = now + 3000
|
|
3333
|
+
}
|
|
3334
|
+
return tabLinkRouterInfoPromise
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
const collectHttpsUrlsFromRouter = (httpUrl, routerData) => {
|
|
3338
|
+
if (!routerData) {
|
|
3339
|
+
return []
|
|
3340
|
+
}
|
|
3341
|
+
let parsed
|
|
3342
|
+
try {
|
|
3343
|
+
parsed = new URL(httpUrl, location.origin)
|
|
3344
|
+
} catch (_) {
|
|
3345
|
+
return []
|
|
3346
|
+
}
|
|
3347
|
+
const protocol = parsed.protocol.toLowerCase()
|
|
3348
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
3349
|
+
return []
|
|
3350
|
+
}
|
|
3351
|
+
let port = parsed.port
|
|
3352
|
+
if (!port) {
|
|
3353
|
+
if (protocol === "http:") {
|
|
3354
|
+
port = "80"
|
|
3355
|
+
} else if (protocol === "https:") {
|
|
3356
|
+
port = "443"
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
const hostLower = parsed.hostname.toLowerCase()
|
|
3360
|
+
const results = new Set()
|
|
3361
|
+
if (port) {
|
|
3362
|
+
const hostPortKey = `${hostLower}:${port}`
|
|
3363
|
+
if (routerData.hostPortMap.has(hostPortKey)) {
|
|
3364
|
+
routerData.hostPortMap.get(hostPortKey).forEach((value) => results.add(value))
|
|
3365
|
+
}
|
|
3366
|
+
if (routerData.portMap.has(port)) {
|
|
3367
|
+
routerData.portMap.get(port).forEach((value) => results.add(value))
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
return Array.from(results)
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
const buildTabLinkEntries = async (link) => {
|
|
3374
|
+
if (!link || !link.href) {
|
|
3375
|
+
return []
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
const baseHref = link.href
|
|
3379
|
+
const entries = []
|
|
3380
|
+
const entryByUrl = new Map()
|
|
3381
|
+
const addEntry = (type, label, url) => {
|
|
3382
|
+
if (!url) {
|
|
3383
|
+
return
|
|
3384
|
+
}
|
|
3385
|
+
const canonical = canonicalizeUrl(url)
|
|
3386
|
+
if (!canonical) {
|
|
3387
|
+
return
|
|
3388
|
+
}
|
|
3389
|
+
if (entryByUrl.has(canonical)) {
|
|
3390
|
+
return
|
|
3391
|
+
}
|
|
3392
|
+
const entry = {
|
|
3393
|
+
type,
|
|
3394
|
+
label,
|
|
3395
|
+
url: canonical,
|
|
3396
|
+
display: formatDisplayUrl(canonical)
|
|
3397
|
+
}
|
|
3398
|
+
entryByUrl.set(canonical, entry)
|
|
3399
|
+
entries.push(entry)
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
if (isHttpUrl(baseHref)) {
|
|
3403
|
+
addEntry("http", "HTTP", baseHref)
|
|
3404
|
+
} else if (isHttpsUrl(baseHref)) {
|
|
3405
|
+
addEntry("https", "HTTPS", baseHref)
|
|
3406
|
+
} else {
|
|
3407
|
+
addEntry("url", "URL", baseHref)
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
const httpCandidates = new Set()
|
|
3411
|
+
const httpsCandidates = new Set()
|
|
3412
|
+
|
|
3413
|
+
if (isHttpUrl(baseHref)) {
|
|
3414
|
+
httpCandidates.add(canonicalizeUrl(baseHref))
|
|
3415
|
+
} else if (isHttpsUrl(baseHref)) {
|
|
3416
|
+
httpsCandidates.add(canonicalizeUrl(baseHref))
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
const scriptKeys = collectScriptKeys(link)
|
|
3420
|
+
if (scriptKeys.length > 0) {
|
|
3421
|
+
const localInfo = await ensureLocalMemory()
|
|
3422
|
+
scriptKeys.forEach((key) => {
|
|
3423
|
+
if (!key) {
|
|
3424
|
+
return
|
|
3425
|
+
}
|
|
3426
|
+
const local = localInfo ? localInfo[key] : undefined
|
|
3427
|
+
if (!local) {
|
|
3428
|
+
return
|
|
3429
|
+
}
|
|
3430
|
+
const urls = collectUrlsFromLocal(local)
|
|
3431
|
+
urls.forEach((value) => {
|
|
3432
|
+
const canonical = canonicalizeUrl(value)
|
|
3433
|
+
if (isHttpsUrl(canonical)) {
|
|
3434
|
+
httpsCandidates.add(canonical)
|
|
3435
|
+
} else if (isHttpUrl(canonical)) {
|
|
3436
|
+
httpCandidates.add(canonical)
|
|
3437
|
+
}
|
|
3438
|
+
})
|
|
3439
|
+
})
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
if (httpCandidates.size > 0) {
|
|
3443
|
+
const routerData = await ensureRouterInfoMapping()
|
|
3444
|
+
httpCandidates.forEach((httpUrl) => {
|
|
3445
|
+
const mapped = collectHttpsUrlsFromRouter(httpUrl, routerData)
|
|
3446
|
+
mapped.forEach((httpsUrl) => {
|
|
3447
|
+
httpsCandidates.add(httpsUrl)
|
|
3448
|
+
})
|
|
3449
|
+
})
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
const httpList = Array.from(httpCandidates).sort()
|
|
3453
|
+
const httpsList = Array.from(httpsCandidates).sort()
|
|
3454
|
+
|
|
3455
|
+
httpList.forEach((url) => {
|
|
3456
|
+
addEntry("http", "HTTP", url)
|
|
3457
|
+
})
|
|
3458
|
+
httpsList.forEach((url) => {
|
|
3459
|
+
addEntry("https", "HTTPS", url)
|
|
3460
|
+
})
|
|
3461
|
+
|
|
3462
|
+
return entries
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
const positionTabLinkPopover = (popover, link) => {
|
|
3466
|
+
if (!popover || !link) {
|
|
3467
|
+
return
|
|
3468
|
+
}
|
|
3469
|
+
const rect = link.getBoundingClientRect()
|
|
3470
|
+
const minWidth = Math.max(rect.width, 260)
|
|
3471
|
+
popover.style.minWidth = `${Math.round(minWidth)}px`
|
|
3472
|
+
popover.style.display = "flex"
|
|
3473
|
+
popover.classList.add("visible")
|
|
3474
|
+
popover.style.visibility = "hidden"
|
|
3475
|
+
|
|
3476
|
+
const popoverWidth = popover.offsetWidth
|
|
3477
|
+
const popoverHeight = popover.offsetHeight
|
|
3478
|
+
|
|
3479
|
+
let left = rect.left
|
|
3480
|
+
let top = rect.bottom + 8
|
|
3481
|
+
|
|
3482
|
+
if (left + popoverWidth > window.innerWidth - 12) {
|
|
3483
|
+
left = window.innerWidth - popoverWidth - 12
|
|
3484
|
+
}
|
|
3485
|
+
if (left < 12) {
|
|
3486
|
+
left = 12
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
if (top + popoverHeight > window.innerHeight - 12) {
|
|
3490
|
+
top = Math.max(12, rect.top - popoverHeight - 8)
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
popover.style.left = `${Math.round(left)}px`
|
|
3494
|
+
popover.style.top = `${Math.round(top)}px`
|
|
3495
|
+
popover.style.visibility = ""
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
const hideTabLinkPopover = ({ immediate = false } = {}) => {
|
|
3499
|
+
const applyHide = () => {
|
|
3500
|
+
if (tabLinkPopoverEl) {
|
|
3501
|
+
tabLinkPopoverEl.classList.remove("visible")
|
|
3502
|
+
tabLinkPopoverEl.style.display = "none"
|
|
3503
|
+
}
|
|
3504
|
+
tabLinkActiveLink = null
|
|
3505
|
+
tabLinkPendingLink = null
|
|
3506
|
+
tabLinkHideTimer = null
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
if (tabLinkHideTimer) {
|
|
3510
|
+
clearTimeout(tabLinkHideTimer)
|
|
3511
|
+
tabLinkHideTimer = null
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
if (immediate) {
|
|
3515
|
+
applyHide()
|
|
3516
|
+
} else {
|
|
3517
|
+
tabLinkHideTimer = setTimeout(applyHide, 120)
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
const renderTabLinkPopover = async (link) => {
|
|
3522
|
+
if (!link || !link.href) {
|
|
3523
|
+
hideTabLinkPopover({ immediate: true })
|
|
3524
|
+
return
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
try {
|
|
3528
|
+
const linkOrigin = new URL(link.href, location.href).origin
|
|
3529
|
+
if (linkOrigin === location.origin) {
|
|
3530
|
+
hideTabLinkPopover({ immediate: true })
|
|
3531
|
+
return
|
|
3532
|
+
}
|
|
3533
|
+
} catch (_) {
|
|
3534
|
+
hideTabLinkPopover({ immediate: true })
|
|
3535
|
+
return
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
if (tabLinkActiveLink === link && tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible")) {
|
|
3539
|
+
if (tabLinkHideTimer) {
|
|
3540
|
+
clearTimeout(tabLinkHideTimer)
|
|
3541
|
+
tabLinkHideTimer = null
|
|
3542
|
+
}
|
|
3543
|
+
return
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
if (tabLinkPendingLink === link && tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible")) {
|
|
3547
|
+
return
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
tabLinkPendingLink = link
|
|
3551
|
+
if (tabLinkHideTimer) {
|
|
3552
|
+
clearTimeout(tabLinkHideTimer)
|
|
3553
|
+
tabLinkHideTimer = null
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
let entries
|
|
3557
|
+
try {
|
|
3558
|
+
entries = await buildTabLinkEntries(link)
|
|
3559
|
+
} catch (_) {
|
|
3560
|
+
tabLinkPendingLink = null
|
|
3561
|
+
return
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
if (tabLinkPendingLink !== link) {
|
|
3565
|
+
return
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
if (!entries || entries.length === 0) {
|
|
3569
|
+
hideTabLinkPopover({ immediate: true })
|
|
3570
|
+
return
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
const popover = ensureTabLinkPopoverEl()
|
|
3574
|
+
popover.innerHTML = ""
|
|
3575
|
+
|
|
3576
|
+
const header = document.createElement("div")
|
|
3577
|
+
header.className = "tab-link-popover-header"
|
|
3578
|
+
header.innerHTML = `<i class="fa-solid fa-arrow-up-right-from-square"></i><span>Open in browser</span>`
|
|
3579
|
+
popover.appendChild(header)
|
|
3580
|
+
|
|
3581
|
+
entries.forEach((entry) => {
|
|
3582
|
+
const item = document.createElement("button")
|
|
3583
|
+
item.type = "button"
|
|
3584
|
+
item.className = "tab-link-popover-item"
|
|
3585
|
+
item.setAttribute("data-url", entry.url)
|
|
3586
|
+
const labelSpan = document.createElement("span")
|
|
3587
|
+
labelSpan.className = "label"
|
|
3588
|
+
labelSpan.textContent = entry.label
|
|
3589
|
+
const valueSpan = document.createElement("span")
|
|
3590
|
+
valueSpan.className = "value"
|
|
3591
|
+
valueSpan.textContent = entry.display
|
|
3592
|
+
item.append(labelSpan, valueSpan)
|
|
3593
|
+
popover.appendChild(item)
|
|
3594
|
+
})
|
|
3595
|
+
|
|
3596
|
+
tabLinkActiveLink = link
|
|
3597
|
+
tabLinkPendingLink = null
|
|
3598
|
+
positionTabLinkPopover(popover, link)
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
const setupTabLinkHover = () => {
|
|
3602
|
+
const container = document.querySelector(".appcanvas > aside .menu-container")
|
|
3603
|
+
if (!container) {
|
|
3604
|
+
return
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
container.addEventListener("mouseover", (event) => {
|
|
3608
|
+
const link = event.target.closest(".frame-link")
|
|
3609
|
+
if (!link || !container.contains(link)) {
|
|
3610
|
+
return
|
|
3611
|
+
}
|
|
3612
|
+
renderTabLinkPopover(link)
|
|
3613
|
+
})
|
|
3614
|
+
|
|
3615
|
+
container.addEventListener("mouseout", (event) => {
|
|
3616
|
+
const origin = event.target.closest(".frame-link")
|
|
3617
|
+
if (!origin || !container.contains(origin)) {
|
|
3618
|
+
return
|
|
3619
|
+
}
|
|
3620
|
+
const related = event.relatedTarget
|
|
3621
|
+
const popover = tabLinkPopoverEl || document.getElementById(TAB_LINK_POPOVER_ID)
|
|
3622
|
+
if (related && (origin.contains(related) || (popover && popover.contains(related)))) {
|
|
3623
|
+
return
|
|
3624
|
+
}
|
|
3625
|
+
hideTabLinkPopover()
|
|
3626
|
+
})
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
const handleGlobalPointer = (event) => {
|
|
3630
|
+
if (!tabLinkPopoverEl || !tabLinkPopoverEl.classList.contains("visible")) {
|
|
3631
|
+
return
|
|
3632
|
+
}
|
|
3633
|
+
if (tabLinkPopoverEl.contains(event.target)) {
|
|
3634
|
+
return
|
|
3635
|
+
}
|
|
3636
|
+
if (tabLinkActiveLink && tabLinkActiveLink.contains(event.target)) {
|
|
3637
|
+
return
|
|
3638
|
+
}
|
|
3639
|
+
hideTabLinkPopover({ immediate: true })
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
window.addEventListener("scroll", () => {
|
|
3643
|
+
if (tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible")) {
|
|
3644
|
+
hideTabLinkPopover({ immediate: true })
|
|
3645
|
+
}
|
|
3646
|
+
}, true)
|
|
3647
|
+
|
|
3648
|
+
window.addEventListener("resize", () => {
|
|
3649
|
+
if (tabLinkPopoverEl && tabLinkPopoverEl.classList.contains("visible") && tabLinkActiveLink) {
|
|
3650
|
+
positionTabLinkPopover(tabLinkPopoverEl, tabLinkActiveLink)
|
|
3651
|
+
}
|
|
3652
|
+
})
|
|
3653
|
+
|
|
3654
|
+
document.addEventListener("mousedown", handleGlobalPointer, true)
|
|
3655
|
+
try {
|
|
3656
|
+
document.addEventListener("touchstart", handleGlobalPointer, { passive: true, capture: true })
|
|
3657
|
+
} catch (_) {
|
|
3658
|
+
document.addEventListener("touchstart", handleGlobalPointer, true)
|
|
3659
|
+
}
|
|
2725
3660
|
const getWindowStorage = () => {
|
|
2726
3661
|
if (typeof windowStorage === "undefined" || !windowStorage) {
|
|
2727
3662
|
return null
|
|
@@ -3938,14 +4873,6 @@ body.dark {
|
|
|
3938
4873
|
})
|
|
3939
4874
|
})
|
|
3940
4875
|
}
|
|
3941
|
-
document.querySelector("#popout-link").addEventListener("click", async (e) => {
|
|
3942
|
-
e.preventDefault()
|
|
3943
|
-
e.stopPropagation()
|
|
3944
|
-
let iframe = document.querySelector("iframe:not(.hidden)")
|
|
3945
|
-
if (iframe) {
|
|
3946
|
-
open_url(iframe.src, "_blank")
|
|
3947
|
-
}
|
|
3948
|
-
})
|
|
3949
4876
|
if (document.querySelector("#edit")) {
|
|
3950
4877
|
document.querySelector("#edit").addEventListener("click", async (e) => {
|
|
3951
4878
|
e.preventDefault()
|
|
@@ -4283,6 +5210,10 @@ body.dark {
|
|
|
4283
5210
|
return
|
|
4284
5211
|
}
|
|
4285
5212
|
|
|
5213
|
+
if (e.target.closest(".frame-link")) {
|
|
5214
|
+
hideTabLinkPopover({ immediate: true })
|
|
5215
|
+
}
|
|
5216
|
+
|
|
4286
5217
|
// 1. handle delete
|
|
4287
5218
|
if (e.target.classList.contains("del")) {
|
|
4288
5219
|
target = e.target
|
|
@@ -4293,6 +5224,7 @@ body.dark {
|
|
|
4293
5224
|
if (target) {
|
|
4294
5225
|
e.preventDefault()
|
|
4295
5226
|
e.stopPropagation()
|
|
5227
|
+
hideTabLinkPopover({ immediate: true })
|
|
4296
5228
|
// Delete
|
|
4297
5229
|
// delete link
|
|
4298
5230
|
let el = target.closest(".frame-link")
|
|
@@ -4310,7 +5242,7 @@ body.dark {
|
|
|
4310
5242
|
}
|
|
4311
5243
|
|
|
4312
5244
|
|
|
4313
|
-
// 2. handle
|
|
5245
|
+
// 2. handle shutdown
|
|
4314
5246
|
if (e.target.classList.contains("shutdown")) {
|
|
4315
5247
|
target = e.target
|
|
4316
5248
|
} else {
|
|
@@ -4539,6 +5471,7 @@ body.dark {
|
|
|
4539
5471
|
container.addEventListener("click", handleMenuClick)
|
|
4540
5472
|
}
|
|
4541
5473
|
})
|
|
5474
|
+
setupTabLinkHover()
|
|
4542
5475
|
document.addEventListener("click", (event) => {
|
|
4543
5476
|
if (event.target.closest("#fs-status .fs-dropdown-menu")) {
|
|
4544
5477
|
return
|
package/server/views/d.ejs
CHANGED
package/server/views/review.ejs
CHANGED
|
@@ -948,7 +948,6 @@ body.dark .top-menu .btn2.selected {
|
|
|
948
948
|
<a class="btn2 <%=type === 'browse' ? 'selected' : ''%>" href="<%=dev_tab%>"><div><i class="fa-solid fa-code"></i></div><div>Dev</div></a>
|
|
949
949
|
<a class="btn2 <%=type === 'run' ? 'selected' : ''%>" href="<%=run_tab%>"><div><i class="fa-solid fa-circle-play"></i></div><div>Run</div></a>
|
|
950
950
|
</div>
|
|
951
|
-
<button class='btn2' id='popout-link' data-tippy-content="open in a web browser"><i class="fa-solid fa-arrow-up-right-from-square"></i></button>
|
|
952
951
|
<a class='btn2' href="/columns" data-tippy-content="split into 2 columns">
|
|
953
952
|
<div><i class="fa-solid fa-table-columns"></i></div>
|
|
954
953
|
</a>
|