pinokiod 3.180.0 → 3.182.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/favicon.js +91 -34
- package/kernel/peer.js +73 -0
- package/kernel/util.js +28 -4
- package/package.json +1 -1
- package/server/index.js +237 -35
- package/server/public/common.js +677 -240
- package/server/public/files-app/app.css +64 -0
- package/server/public/files-app/app.js +87 -0
- package/server/public/install.js +8 -1
- package/server/public/layout.js +124 -0
- package/server/public/nav.js +227 -64
- package/server/public/sound/beep.mp3 +0 -0
- package/server/public/sound/bell.mp3 +0 -0
- package/server/public/sound/bright-ring.mp3 +0 -0
- package/server/public/sound/clap.mp3 +0 -0
- package/server/public/sound/deep-ring.mp3 +0 -0
- package/server/public/sound/gasp.mp3 +0 -0
- package/server/public/sound/hehe.mp3 +0 -0
- package/server/public/sound/levelup.mp3 +0 -0
- package/server/public/sound/light-pop.mp3 +0 -0
- package/server/public/sound/light-ring.mp3 +0 -0
- package/server/public/sound/meow.mp3 +0 -0
- package/server/public/sound/piano.mp3 +0 -0
- package/server/public/sound/pop.mp3 +0 -0
- package/server/public/sound/uhoh.mp3 +0 -0
- package/server/public/sound/whistle.mp3 +0 -0
- package/server/public/style.css +195 -4
- package/server/public/tab-idle-notifier.js +700 -4
- package/server/public/terminal-settings.js +1131 -0
- package/server/public/urldropdown.css +28 -1
- package/server/socket.js +71 -4
- package/server/views/{terminals.ejs → agents.ejs} +108 -32
- package/server/views/app.ejs +321 -104
- package/server/views/bootstrap.ejs +8 -0
- package/server/views/connect.ejs +10 -1
- package/server/views/d.ejs +172 -18
- package/server/views/editor.ejs +8 -0
- package/server/views/file_browser.ejs +4 -0
- package/server/views/index.ejs +10 -1
- package/server/views/init/index.ejs +18 -3
- package/server/views/install.ejs +8 -0
- package/server/views/layout.ejs +2 -0
- package/server/views/net.ejs +10 -1
- package/server/views/network.ejs +10 -1
- package/server/views/pro.ejs +8 -0
- package/server/views/prototype/index.ejs +8 -0
- package/server/views/screenshots.ejs +10 -2
- package/server/views/settings.ejs +10 -2
- package/server/views/shell.ejs +8 -0
- package/server/views/terminal.ejs +8 -0
- package/server/views/tools.ejs +10 -2
package/server/views/app.ejs
CHANGED
|
@@ -891,6 +891,14 @@ body.dark .tab-link-popover {
|
|
|
891
891
|
body.dark .tab-link-popover .tab-link-popover-header {
|
|
892
892
|
color: rgba(226, 232, 240, 0.7);
|
|
893
893
|
}
|
|
894
|
+
.tab-link-popover .tab-link-popover-separator {
|
|
895
|
+
height: 1px;
|
|
896
|
+
margin: 4px 14px;
|
|
897
|
+
background: rgba(15, 23, 42, 0.08);
|
|
898
|
+
}
|
|
899
|
+
body.dark .tab-link-popover .tab-link-popover-separator {
|
|
900
|
+
background: rgba(148, 163, 184, 0.18);
|
|
901
|
+
}
|
|
894
902
|
.tab-link-popover .tab-link-popover-item {
|
|
895
903
|
width: 100%;
|
|
896
904
|
border: none;
|
|
@@ -905,6 +913,9 @@ body.dark .tab-link-popover .tab-link-popover-header {
|
|
|
905
913
|
background: transparent;
|
|
906
914
|
cursor: pointer;
|
|
907
915
|
}
|
|
916
|
+
.tab-link-popover .tab-link-popover-item.qr-inline { flex-direction: row; align-items: center; gap: 10px; }
|
|
917
|
+
.tab-link-popover .tab-link-popover-item.qr-inline .textcol { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
|
|
918
|
+
.tab-link-popover .tab-link-popover-item.qr-inline .qr { width: 64px; height: 64px; image-rendering: pixelated; flex: 0 0 auto; margin-left: auto; }
|
|
908
919
|
.tab-link-popover .tab-link-popover-item:hover,
|
|
909
920
|
.tab-link-popover .tab-link-popover-item:focus-visible {
|
|
910
921
|
background: rgba(15, 23, 42, 0.06);
|
|
@@ -921,6 +932,10 @@ body.dark .tab-link-popover .tab-link-popover-item:focus-visible {
|
|
|
921
932
|
text-transform: uppercase;
|
|
922
933
|
color: rgba(15, 23, 42, 0.55);
|
|
923
934
|
}
|
|
935
|
+
.tab-link-popover .tab-link-popover-item .label i {
|
|
936
|
+
margin-right: 6px;
|
|
937
|
+
font-size: 11px;
|
|
938
|
+
}
|
|
924
939
|
body.dark .tab-link-popover .tab-link-popover-item .label {
|
|
925
940
|
color: rgba(226, 232, 240, 0.65);
|
|
926
941
|
}
|
|
@@ -2232,6 +2247,19 @@ body.dark {
|
|
|
2232
2247
|
cursor: not-allowed;
|
|
2233
2248
|
}
|
|
2234
2249
|
|
|
2250
|
+
.pinokio-history-actions {
|
|
2251
|
+
display: inline-flex;
|
|
2252
|
+
align-items: center;
|
|
2253
|
+
gap: 10px;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
.pinokio-history-branch-select.pinokio-modal-input {
|
|
2257
|
+
width: auto;
|
|
2258
|
+
min-width: 160px;
|
|
2259
|
+
padding: 6px 10px;
|
|
2260
|
+
font-size: 13px;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2235
2263
|
.pinokio-pill {
|
|
2236
2264
|
display: inline-flex;
|
|
2237
2265
|
align-items: center;
|
|
@@ -2957,6 +2985,15 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
2957
2985
|
overflow: auto;
|
|
2958
2986
|
flex-wrap: nowrap;
|
|
2959
2987
|
}
|
|
2988
|
+
/* Keep minimized header horizontal and compact on small screens */
|
|
2989
|
+
header.navheader.minimized,
|
|
2990
|
+
header.navheader.minimized h1 {
|
|
2991
|
+
display: inline-flex;
|
|
2992
|
+
flex-direction: row;
|
|
2993
|
+
}
|
|
2994
|
+
header.navheader.minimized h1 .btn2 {
|
|
2995
|
+
width: auto;
|
|
2996
|
+
}
|
|
2960
2997
|
.appcanvas {
|
|
2961
2998
|
margin-left: 0;
|
|
2962
2999
|
flex: 1 1 auto;
|
|
@@ -3508,6 +3545,8 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3508
3545
|
return false
|
|
3509
3546
|
}
|
|
3510
3547
|
|
|
3548
|
+
const isIPv4Host = (host) => /^(\d{1,3}\.){3}\d{1,3}$/.test((host || '').trim())
|
|
3549
|
+
|
|
3511
3550
|
const extractProjectSlug = (node) => {
|
|
3512
3551
|
if (!node) {
|
|
3513
3552
|
return ""
|
|
@@ -3662,22 +3701,36 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3662
3701
|
if (!trimmed) {
|
|
3663
3702
|
return ""
|
|
3664
3703
|
}
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3704
|
+
// If it's already a URL, ensure it's HTTPS and not an IP host
|
|
3705
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
3706
|
+
try {
|
|
3707
|
+
const parsed = new URL(trimmed)
|
|
3708
|
+
const host = (parsed.hostname || '').toLowerCase()
|
|
3709
|
+
if (!host || isIPv4Host(host)) {
|
|
3710
|
+
return ""
|
|
3711
|
+
}
|
|
3712
|
+
// Only accept domains (prefer *.localhost) for HTTPS targets
|
|
3713
|
+
if (!(host === 'localhost' || host.endsWith('.localhost') || host.includes('.'))) {
|
|
3714
|
+
return ""
|
|
3715
|
+
}
|
|
3716
|
+
let pathname = parsed.pathname || ""
|
|
3717
|
+
if (pathname === "/") pathname = ""
|
|
3718
|
+
const search = parsed.search || ""
|
|
3719
|
+
return `https://${host}${pathname}${search}`
|
|
3720
|
+
} catch (_) {
|
|
3721
|
+
return ""
|
|
3722
|
+
}
|
|
3669
3723
|
}
|
|
3724
|
+
// Not a full URL: accept plain domains (prefer *.localhost), reject IPs
|
|
3670
3725
|
try {
|
|
3671
|
-
const
|
|
3672
|
-
if (!
|
|
3726
|
+
const hostCandidate = trimmed.split('/')[0].toLowerCase()
|
|
3727
|
+
if (!hostCandidate || isIPv4Host(hostCandidate)) {
|
|
3673
3728
|
return ""
|
|
3674
3729
|
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
pathname = ""
|
|
3730
|
+
if (!(hostCandidate === 'localhost' || hostCandidate.endsWith('.localhost') || hostCandidate.includes('.'))) {
|
|
3731
|
+
return ""
|
|
3678
3732
|
}
|
|
3679
|
-
|
|
3680
|
-
return `https://${parsed.host}${pathname}${search}`
|
|
3733
|
+
return `https://${hostCandidate}`
|
|
3681
3734
|
} catch (_) {
|
|
3682
3735
|
return ""
|
|
3683
3736
|
}
|
|
@@ -3736,7 +3789,8 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3736
3789
|
const ensureRouterInfoMapping = async () => {
|
|
3737
3790
|
const now = Date.now()
|
|
3738
3791
|
if (!tabLinkRouterInfoPromise || now > tabLinkRouterInfoExpiry) {
|
|
3739
|
-
|
|
3792
|
+
// Use lightweight router mapping to avoid favicon/installed overhead
|
|
3793
|
+
tabLinkRouterInfoPromise = fetch("/info/router", {
|
|
3740
3794
|
method: "GET",
|
|
3741
3795
|
headers: {
|
|
3742
3796
|
"Accept": "application/json"
|
|
@@ -3758,6 +3812,8 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3758
3812
|
: []
|
|
3759
3813
|
const portMap = new Map()
|
|
3760
3814
|
const hostPortMap = new Map()
|
|
3815
|
+
const externalHttpByExtPort = new Map() // ext port -> Set of host:port (external_ip)
|
|
3816
|
+
const externalHttpByIntPort = new Map() // internal port -> Set of host:port (external_ip)
|
|
3761
3817
|
const hostAliasPortMap = new Map()
|
|
3762
3818
|
if (data?.router && typeof data.router === "object") {
|
|
3763
3819
|
Object.entries(data.router).forEach(([dial, hosts]) => {
|
|
@@ -3902,11 +3958,29 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3902
3958
|
mergeTargets(entry.external_domain)
|
|
3903
3959
|
mergeTargets(entry.https_href)
|
|
3904
3960
|
mergeTargets(entry.app_href)
|
|
3905
|
-
|
|
3906
|
-
mergeTargets(entry.internal_router)
|
|
3907
|
-
mergeTargets(entry.match)
|
|
3908
|
-
mergeTargets(entry.host)
|
|
3961
|
+
// Some rewrite mapping entries expose domain candidates under `hosts`
|
|
3909
3962
|
mergeTargets(entry.hosts)
|
|
3963
|
+
// Internal router can also include domain aliases (e.g., comfyui.localhost)
|
|
3964
|
+
mergeTargets(entry.internal_router)
|
|
3965
|
+
|
|
3966
|
+
// Record external http host:port candidates by external and internal ports for later
|
|
3967
|
+
if (entry.external_ip && typeof entry.external_ip === 'string') {
|
|
3968
|
+
const parsed = parseHostPort(entry.external_ip)
|
|
3969
|
+
if (parsed && parsed.port) {
|
|
3970
|
+
const keyExt = parsed.port
|
|
3971
|
+
if (!externalHttpByExtPort.has(keyExt)) {
|
|
3972
|
+
externalHttpByExtPort.set(keyExt, new Set())
|
|
3973
|
+
}
|
|
3974
|
+
externalHttpByExtPort.get(keyExt).add(`${parsed.host}:${parsed.port}`)
|
|
3975
|
+
const keyInt = String(entry.internal_port || '')
|
|
3976
|
+
if (keyInt) {
|
|
3977
|
+
if (!externalHttpByIntPort.has(keyInt)) {
|
|
3978
|
+
externalHttpByIntPort.set(keyInt, new Set())
|
|
3979
|
+
}
|
|
3980
|
+
externalHttpByIntPort.get(keyInt).add(`${parsed.host}:${parsed.port}`)
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3910
3984
|
|
|
3911
3985
|
if (httpsTargets.size === 0) {
|
|
3912
3986
|
return
|
|
@@ -3982,14 +4056,18 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
3982
4056
|
|
|
3983
4057
|
return {
|
|
3984
4058
|
portMap,
|
|
3985
|
-
hostPortMap
|
|
4059
|
+
hostPortMap,
|
|
4060
|
+
externalHttpByExtPort,
|
|
4061
|
+
externalHttpByIntPort
|
|
3986
4062
|
}
|
|
3987
4063
|
})
|
|
3988
4064
|
.catch(() => {
|
|
3989
4065
|
tabLinkRouterHttpsActive = null
|
|
3990
4066
|
return {
|
|
3991
4067
|
portMap: new Map(),
|
|
3992
|
-
hostPortMap: new Map()
|
|
4068
|
+
hostPortMap: new Map(),
|
|
4069
|
+
externalHttpByExtPort: new Map(),
|
|
4070
|
+
externalHttpByIntPort: new Map()
|
|
3993
4071
|
}
|
|
3994
4072
|
})
|
|
3995
4073
|
tabLinkRouterInfoExpiry = now + 3000
|
|
@@ -4042,7 +4120,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4042
4120
|
const projectSlug = extractProjectSlug(link).toLowerCase()
|
|
4043
4121
|
const entries = []
|
|
4044
4122
|
const entryByUrl = new Map()
|
|
4045
|
-
const addEntry = (type, label, url) => {
|
|
4123
|
+
const addEntry = (type, label, url, opts = {}) => {
|
|
4046
4124
|
if (!url) {
|
|
4047
4125
|
return
|
|
4048
4126
|
}
|
|
@@ -4067,13 +4145,16 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4067
4145
|
return
|
|
4068
4146
|
}
|
|
4069
4147
|
if (entryByUrl.has(canonical)) {
|
|
4148
|
+
const existing = entryByUrl.get(canonical)
|
|
4149
|
+
if (opts && opts.qr === true) existing.qr = true
|
|
4070
4150
|
return
|
|
4071
4151
|
}
|
|
4072
4152
|
const entry = {
|
|
4073
4153
|
type,
|
|
4074
4154
|
label,
|
|
4075
4155
|
url: canonical,
|
|
4076
|
-
display: formatDisplayUrl(canonical)
|
|
4156
|
+
display: formatDisplayUrl(canonical),
|
|
4157
|
+
qr: opts && opts.qr === true
|
|
4077
4158
|
}
|
|
4078
4159
|
entryByUrl.set(canonical, entry)
|
|
4079
4160
|
entries.push(entry)
|
|
@@ -4087,11 +4168,11 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4087
4168
|
addEntry("url", "URL", baseHref)
|
|
4088
4169
|
}
|
|
4089
4170
|
|
|
4090
|
-
const httpCandidates = new
|
|
4171
|
+
const httpCandidates = new Map() // url -> { qr: boolean }
|
|
4091
4172
|
const httpsCandidates = new Set()
|
|
4092
4173
|
|
|
4093
4174
|
if (isHttpUrl(baseHref)) {
|
|
4094
|
-
httpCandidates.
|
|
4175
|
+
httpCandidates.set(canonicalizeUrl(baseHref), { qr: false })
|
|
4095
4176
|
} else if (isHttpsUrl(baseHref)) {
|
|
4096
4177
|
httpsCandidates.add(canonicalizeUrl(baseHref))
|
|
4097
4178
|
}
|
|
@@ -4109,7 +4190,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4109
4190
|
const normalizedPath = pathname.toLowerCase()
|
|
4110
4191
|
if (normalizedPath.includes(`/asset/api/${projectSlug}`)) {
|
|
4111
4192
|
const fallbackHttp = `http://127.0.0.1:42000${pathname}`
|
|
4112
|
-
httpCandidates.
|
|
4193
|
+
httpCandidates.set(canonicalizeUrl(fallbackHttp), { qr: false })
|
|
4113
4194
|
}
|
|
4114
4195
|
} catch (_) {
|
|
4115
4196
|
// ignore fallback errors
|
|
@@ -4133,15 +4214,16 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4133
4214
|
if (isHttpsUrl(canonical)) {
|
|
4134
4215
|
httpsCandidates.add(canonical)
|
|
4135
4216
|
} else if (isHttpUrl(canonical)) {
|
|
4136
|
-
httpCandidates.
|
|
4217
|
+
const prev = httpCandidates.get(canonical)
|
|
4218
|
+
httpCandidates.set(canonical, { qr: prev ? prev.qr === true : false })
|
|
4137
4219
|
}
|
|
4138
4220
|
})
|
|
4139
4221
|
})
|
|
4140
4222
|
}
|
|
4141
4223
|
|
|
4224
|
+
const routerData = await ensureRouterInfoMapping()
|
|
4142
4225
|
if (httpCandidates.size > 0) {
|
|
4143
|
-
|
|
4144
|
-
httpCandidates.forEach((httpUrl) => {
|
|
4226
|
+
Array.from(httpCandidates.keys()).forEach((httpUrl) => {
|
|
4145
4227
|
const mapped = collectHttpsUrlsFromRouter(httpUrl, routerData)
|
|
4146
4228
|
mapped.forEach((httpsUrl) => {
|
|
4147
4229
|
httpsCandidates.add(httpsUrl)
|
|
@@ -4149,6 +4231,28 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4149
4231
|
})
|
|
4150
4232
|
}
|
|
4151
4233
|
|
|
4234
|
+
// Add external 192.168.* http host:port candidates mapped from the same internal port as base HTTP
|
|
4235
|
+
try {
|
|
4236
|
+
const base = new URL(baseHref, location.origin)
|
|
4237
|
+
let basePort = base.port
|
|
4238
|
+
if (!basePort) {
|
|
4239
|
+
basePort = base.protocol.toLowerCase() === 'https:' ? '443' : '80'
|
|
4240
|
+
}
|
|
4241
|
+
const samePortHosts = routerData && routerData.externalHttpByIntPort ? routerData.externalHttpByIntPort.get(basePort) : null
|
|
4242
|
+
if (samePortHosts && samePortHosts.size > 0) {
|
|
4243
|
+
samePortHosts.forEach((hostport) => {
|
|
4244
|
+
try {
|
|
4245
|
+
const hpUrl = `http://${hostport}${base.pathname || '/'}${base.search || ''}`
|
|
4246
|
+
const canonical = canonicalizeUrl(hpUrl)
|
|
4247
|
+
if (isHttpUrl(canonical)) {
|
|
4248
|
+
const prev = httpCandidates.get(canonical)
|
|
4249
|
+
httpCandidates.set(canonical, { qr: true || (prev ? prev.qr === true : false) })
|
|
4250
|
+
}
|
|
4251
|
+
} catch (_) {}
|
|
4252
|
+
})
|
|
4253
|
+
}
|
|
4254
|
+
} catch (_) {}
|
|
4255
|
+
|
|
4152
4256
|
const httpsList = Array.from(httpsCandidates).sort()
|
|
4153
4257
|
|
|
4154
4258
|
if (httpsList.length > 0) {
|
|
@@ -4163,17 +4267,20 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4163
4267
|
}
|
|
4164
4268
|
const hostPort = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname
|
|
4165
4269
|
const httpUrl = `http://${hostPort}${parsed.pathname || "/"}${parsed.search || ""}`
|
|
4166
|
-
|
|
4270
|
+
const key = canonicalizeUrl(httpUrl)
|
|
4271
|
+
const prev = httpCandidates.get(key)
|
|
4272
|
+
httpCandidates.set(key, { qr: prev ? prev.qr === true : false })
|
|
4167
4273
|
} catch (_) {
|
|
4168
4274
|
// ignore failures
|
|
4169
4275
|
}
|
|
4170
4276
|
})
|
|
4171
4277
|
}
|
|
4172
4278
|
|
|
4173
|
-
const httpList = Array.from(httpCandidates).sort()
|
|
4279
|
+
const httpList = Array.from(httpCandidates.keys()).sort()
|
|
4174
4280
|
|
|
4175
4281
|
httpList.forEach((url) => {
|
|
4176
|
-
|
|
4282
|
+
const meta = httpCandidates.get(url) || { qr: false }
|
|
4283
|
+
addEntry("http", "HTTP", url, { qr: meta.qr === true })
|
|
4177
4284
|
})
|
|
4178
4285
|
httpsList.forEach((url) => {
|
|
4179
4286
|
addEntry("https", "HTTPS", url)
|
|
@@ -4273,27 +4380,47 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4273
4380
|
tabLinkHideTimer = null
|
|
4274
4381
|
}
|
|
4275
4382
|
|
|
4276
|
-
|
|
4383
|
+
// Show lightweight loading popover immediately while mapping fetch runs
|
|
4384
|
+
try {
|
|
4385
|
+
const pop = ensureTabLinkPopoverEl()
|
|
4386
|
+
pop.innerHTML = ''
|
|
4387
|
+
const header = document.createElement('div')
|
|
4388
|
+
header.className = 'tab-link-popover-header'
|
|
4389
|
+
header.innerHTML = `<i class="fa-solid fa-arrow-up-right-from-square"></i><span>Open in browser</span>`
|
|
4390
|
+
const item = document.createElement('div')
|
|
4391
|
+
item.className = 'tab-link-popover-item'
|
|
4392
|
+
const label = document.createElement('span')
|
|
4393
|
+
label.className = 'label'
|
|
4394
|
+
label.textContent = 'Loading…'
|
|
4395
|
+
const value = document.createElement('span')
|
|
4396
|
+
value.className = 'value muted'
|
|
4397
|
+
value.textContent = 'Discovering routes'
|
|
4398
|
+
item.append(label, value)
|
|
4399
|
+
pop.append(header, item)
|
|
4400
|
+
positionTabLinkPopover(pop, link)
|
|
4401
|
+
} catch (_) {}
|
|
4402
|
+
|
|
4403
|
+
let entries = []
|
|
4277
4404
|
try {
|
|
4278
|
-
|
|
4405
|
+
const result = await buildTabLinkEntries(link)
|
|
4406
|
+
if (Array.isArray(result)) {
|
|
4407
|
+
entries = result.slice()
|
|
4408
|
+
}
|
|
4279
4409
|
} catch (_) {
|
|
4280
|
-
|
|
4281
|
-
return
|
|
4410
|
+
entries = []
|
|
4282
4411
|
}
|
|
4283
4412
|
|
|
4284
4413
|
if (tabLinkPendingLink !== link) {
|
|
4285
4414
|
return
|
|
4286
4415
|
}
|
|
4287
4416
|
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
return
|
|
4291
|
-
}
|
|
4417
|
+
let openEntries = Array.isArray(entries) ? entries.slice() : []
|
|
4418
|
+
let showOpenSection = openEntries.length > 0
|
|
4292
4419
|
|
|
4293
4420
|
if (sameOrigin) {
|
|
4294
4421
|
const slug = extractProjectSlug(link).toLowerCase()
|
|
4295
4422
|
if (slug) {
|
|
4296
|
-
|
|
4423
|
+
openEntries = openEntries.filter((entry) => {
|
|
4297
4424
|
if (!entry || !entry.url) {
|
|
4298
4425
|
return false
|
|
4299
4426
|
}
|
|
@@ -4328,17 +4455,21 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4328
4455
|
return false
|
|
4329
4456
|
})
|
|
4330
4457
|
} else {
|
|
4331
|
-
|
|
4458
|
+
openEntries = openEntries.filter((entry) => entry.url === canonicalBase)
|
|
4332
4459
|
}
|
|
4333
4460
|
|
|
4334
|
-
const hasAlternate =
|
|
4461
|
+
const hasAlternate = openEntries.some((entry) => entry.url !== canonicalBase)
|
|
4335
4462
|
if (!hasAlternate) {
|
|
4336
|
-
|
|
4337
|
-
|
|
4463
|
+
showOpenSection = false
|
|
4464
|
+
openEntries = []
|
|
4465
|
+
} else {
|
|
4466
|
+
showOpenSection = openEntries.length > 0
|
|
4338
4467
|
}
|
|
4468
|
+
} else {
|
|
4469
|
+
showOpenSection = openEntries.length > 0
|
|
4339
4470
|
}
|
|
4340
4471
|
|
|
4341
|
-
if (!
|
|
4472
|
+
if (!showOpenSection && !canonicalBase) {
|
|
4342
4473
|
hideTabLinkPopover({ immediate: true })
|
|
4343
4474
|
return
|
|
4344
4475
|
}
|
|
@@ -4346,46 +4477,93 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
4346
4477
|
const popover = ensureTabLinkPopoverEl()
|
|
4347
4478
|
popover.innerHTML = ""
|
|
4348
4479
|
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4480
|
+
if (showOpenSection) {
|
|
4481
|
+
const header = document.createElement("div")
|
|
4482
|
+
header.className = "tab-link-popover-header"
|
|
4483
|
+
header.innerHTML = `<i class="fa-solid fa-arrow-up-right-from-square"></i><span>Open in browser</span>`
|
|
4484
|
+
popover.appendChild(header)
|
|
4485
|
+
|
|
4486
|
+
const hasHttpsEntry = openEntries.some((entry) => entry && entry.type === "https")
|
|
4487
|
+
|
|
4488
|
+
openEntries.forEach((entry) => {
|
|
4489
|
+
const item = document.createElement("button")
|
|
4490
|
+
item.type = "button"
|
|
4491
|
+
item.setAttribute("data-url", entry.url)
|
|
4492
|
+
const labelSpan = document.createElement("span")
|
|
4493
|
+
labelSpan.className = "label"
|
|
4494
|
+
labelSpan.textContent = entry.label
|
|
4495
|
+
const valueSpan = document.createElement("span")
|
|
4496
|
+
valueSpan.className = "value"
|
|
4497
|
+
valueSpan.textContent = entry.display
|
|
4498
|
+
|
|
4499
|
+
if (entry.type === 'http' && entry.qr === true) {
|
|
4500
|
+
item.className = "tab-link-popover-item qr-inline"
|
|
4501
|
+
const textCol = document.createElement('div')
|
|
4502
|
+
textCol.className = 'textcol'
|
|
4503
|
+
textCol.append(labelSpan, valueSpan)
|
|
4504
|
+
const qrImg = document.createElement('img')
|
|
4505
|
+
qrImg.className = 'qr'
|
|
4506
|
+
qrImg.alt = 'QR'
|
|
4507
|
+
qrImg.decoding = 'async'
|
|
4508
|
+
qrImg.loading = 'lazy'
|
|
4509
|
+
qrImg.src = `/qr?data=${encodeURIComponent(entry.url)}&s=4&m=0`
|
|
4510
|
+
item.append(textCol, qrImg)
|
|
4511
|
+
} else {
|
|
4512
|
+
item.className = "tab-link-popover-item"
|
|
4513
|
+
// Keep label and value as direct children so column layout applies
|
|
4514
|
+
item.append(labelSpan, valueSpan)
|
|
4515
|
+
}
|
|
4516
|
+
popover.appendChild(item)
|
|
4517
|
+
})
|
|
4518
|
+
|
|
4519
|
+
if (tabLinkRouterHttpsActive === false && !hasHttpsEntry) {
|
|
4520
|
+
const footerButton = document.createElement("button")
|
|
4521
|
+
footerButton.type = "button"
|
|
4522
|
+
footerButton.className = "tab-link-popover-item tab-link-popover-footer"
|
|
4523
|
+
footerButton.setAttribute("data-url", "/network")
|
|
4524
|
+
footerButton.setAttribute("data-target", "_self")
|
|
4525
|
+
footerButton.setAttribute("aria-label", "Open network settings to configure local HTTPS")
|
|
4370
4526
|
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
footerButton.className = "tab-link-popover-item tab-link-popover-footer"
|
|
4375
|
-
footerButton.setAttribute("data-url", "/network")
|
|
4376
|
-
footerButton.setAttribute("data-target", "_self")
|
|
4377
|
-
footerButton.setAttribute("aria-label", "Open network settings to configure local HTTPS")
|
|
4527
|
+
const footerLabel = document.createElement("span")
|
|
4528
|
+
footerLabel.className = "label"
|
|
4529
|
+
footerLabel.textContent = "Custom domain not active"
|
|
4378
4530
|
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4531
|
+
const footerValue = document.createElement("span")
|
|
4532
|
+
footerValue.className = "value"
|
|
4533
|
+
footerValue.textContent = "Click to activate"
|
|
4382
4534
|
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4535
|
+
footerButton.append(footerLabel, footerValue)
|
|
4536
|
+
popover.appendChild(footerButton)
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4386
4539
|
|
|
4387
|
-
|
|
4388
|
-
|
|
4540
|
+
if (showOpenSection && canonicalBase) {
|
|
4541
|
+
const separator = document.createElement("div")
|
|
4542
|
+
separator.className = "tab-link-popover-separator"
|
|
4543
|
+
popover.appendChild(separator)
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
if (canonicalBase) {
|
|
4547
|
+
const debugHeader = document.createElement("div")
|
|
4548
|
+
debugHeader.className = "tab-link-popover-header"
|
|
4549
|
+
debugHeader.innerHTML = `<i class="fa-solid fa-cube"></i><span>Debug</span>`
|
|
4550
|
+
popover.appendChild(debugHeader)
|
|
4551
|
+
|
|
4552
|
+
const debugItem = document.createElement("button")
|
|
4553
|
+
debugItem.type = "button"
|
|
4554
|
+
debugItem.className = "tab-link-popover-item"
|
|
4555
|
+
debugItem.setAttribute("data-url", canonicalBase)
|
|
4556
|
+
|
|
4557
|
+
const debugLabel = document.createElement("span")
|
|
4558
|
+
debugLabel.className = "label"
|
|
4559
|
+
debugLabel.innerHTML = `<i class="fa-solid fa-border-none"></i><span>Select a region</span>`
|
|
4560
|
+
|
|
4561
|
+
const debugValue = document.createElement("span")
|
|
4562
|
+
debugValue.className = "value"
|
|
4563
|
+
debugValue.textContent = formatDisplayUrl(canonicalBase)
|
|
4564
|
+
|
|
4565
|
+
debugItem.append(debugLabel, debugValue)
|
|
4566
|
+
popover.appendChild(debugItem)
|
|
4389
4567
|
}
|
|
4390
4568
|
|
|
4391
4569
|
tabLinkActiveLink = link
|
|
@@ -7879,6 +8057,10 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
7879
8057
|
const commits = historyData.log || []
|
|
7880
8058
|
const remote = historyData.remote || ''
|
|
7881
8059
|
const currentRef = historyData.ref || 'HEAD'
|
|
8060
|
+
const branchEntries = Array.isArray(historyData.branches) ? historyData.branches : []
|
|
8061
|
+
const isOid = (s) => typeof s === 'string' && /^[0-9a-f]{7,40}$/i.test(s)
|
|
8062
|
+
const realBranchEntries = branchEntries.filter((entry) => entry && typeof entry.branch === 'string' && entry.branch.length > 0 && !isOid(entry.branch))
|
|
8063
|
+
const selectedBranchName = (historyData && typeof historyData.branch === 'string' && !isOid(historyData.branch)) ? historyData.branch : null
|
|
7882
8064
|
|
|
7883
8065
|
const commitCountLabel = `${commits.length} commit${commits.length === 1 ? '' : 's'}`
|
|
7884
8066
|
const lastCommit = commits[0]
|
|
@@ -7910,9 +8092,21 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
7910
8092
|
<div class="pinokio-history-latest-text">
|
|
7911
8093
|
Currently viewing ${escapeHtml(currentRef)}
|
|
7912
8094
|
</div>
|
|
7913
|
-
<
|
|
7914
|
-
<
|
|
7915
|
-
|
|
8095
|
+
<div class="pinokio-history-actions">
|
|
8096
|
+
<button type="button" class="pinokio-history-latest-btn" data-history-return-head>
|
|
8097
|
+
<i class="fa-solid fa-arrow-rotate-left"></i> Return to newest commit
|
|
8098
|
+
</button>
|
|
8099
|
+
${realBranchEntries.length ? `
|
|
8100
|
+
<select class="pinokio-modal-input pinokio-history-branch-select" data-history-branch-select aria-label="Select branch">
|
|
8101
|
+
${realBranchEntries.map(e => `
|
|
8102
|
+
<option value="${escapeHtml(e.branch)}"${selectedBranchName === e.branch ? ' selected' : ''}>${escapeHtml(e.branch)}</option>
|
|
8103
|
+
`).join('')}
|
|
8104
|
+
</select>
|
|
8105
|
+
<button type="button" class="pinokio-history-latest-btn" data-history-branch-switch>
|
|
8106
|
+
<i class="fa-solid fa-code-branch"></i> Switch
|
|
8107
|
+
</button>
|
|
8108
|
+
` : ''}
|
|
8109
|
+
</div>
|
|
7916
8110
|
</div>
|
|
7917
8111
|
<div class="pinokio-modal-body pinokio-modal-body--history">
|
|
7918
8112
|
${commits.length === 0 ?
|
|
@@ -7949,7 +8143,9 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
7949
8143
|
|
|
7950
8144
|
const returnBtn = document.querySelector('[data-history-return-head]')
|
|
7951
8145
|
const banner = document.querySelector('[data-history-latest-banner]')
|
|
7952
|
-
|
|
8146
|
+
const branchSelect = document.querySelector('[data-history-branch-select]')
|
|
8147
|
+
const branchSwitchBtn = document.querySelector('[data-history-branch-switch]')
|
|
8148
|
+
if (typeof showIframeView === 'function') {
|
|
7953
8149
|
const repoParam = options && typeof options === 'object' ? options.repoParam : null
|
|
7954
8150
|
let checkoutCwd = null
|
|
7955
8151
|
if (historyData && typeof historyData.dir === 'string' && historyData.dir.length > 0) {
|
|
@@ -7958,31 +8154,52 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
7958
8154
|
checkoutCwd = repoParam
|
|
7959
8155
|
}
|
|
7960
8156
|
|
|
8157
|
+
const isOid = (s) => typeof s === 'string' && /^[0-9a-f]{7,40}$/i.test(s)
|
|
7961
8158
|
const branchEntries = Array.isArray(historyData.branches) ? historyData.branches : []
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
if (
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
8159
|
+
const realBranches = branchEntries
|
|
8160
|
+
.map((entry) => entry && typeof entry.branch === 'string' ? entry.branch : null)
|
|
8161
|
+
.filter((name) => name && !isOid(name))
|
|
8162
|
+
|
|
8163
|
+
// Wire the branch selector
|
|
8164
|
+
if (branchSelect && checkoutCwd) {
|
|
8165
|
+
branchSelect.addEventListener('change', () => {
|
|
8166
|
+
const value = branchSelect.value
|
|
8167
|
+
if (!value) return
|
|
8168
|
+
const url = `/run/scripts/git/checkout.json?cwd=${encodeURIComponent(checkoutCwd)}&commit=${encodeURIComponent(value)}&callback_target=parent&callback=$location.href`
|
|
8169
|
+
openCheckoutTerminal(url)
|
|
8170
|
+
})
|
|
7972
8171
|
}
|
|
7973
|
-
if (
|
|
7974
|
-
|
|
8172
|
+
if (branchSwitchBtn && branchSelect && checkoutCwd) {
|
|
8173
|
+
branchSwitchBtn.addEventListener('click', () => {
|
|
8174
|
+
const value = branchSelect.value
|
|
8175
|
+
if (!value) return
|
|
8176
|
+
const url = `/run/scripts/git/checkout.json?cwd=${encodeURIComponent(checkoutCwd)}&commit=${encodeURIComponent(value)}&callback_target=parent&callback=$location.href`
|
|
8177
|
+
openCheckoutTerminal(url)
|
|
8178
|
+
})
|
|
7975
8179
|
}
|
|
7976
8180
|
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
if (
|
|
7985
|
-
|
|
8181
|
+
// Fix "Return to newest commit" target selection
|
|
8182
|
+
if (returnBtn) {
|
|
8183
|
+
let checkoutTarget = null
|
|
8184
|
+
if (repoData && typeof repoData.branch === 'string' && repoData.branch.length > 0 && !isOid(repoData.branch)) {
|
|
8185
|
+
checkoutTarget = repoData.branch
|
|
8186
|
+
} else if (historyData && typeof historyData.branch === 'string' && historyData.branch.length > 0 && !isOid(historyData.branch)) {
|
|
8187
|
+
checkoutTarget = historyData.branch
|
|
8188
|
+
} else if (realBranches.length > 0) {
|
|
8189
|
+
const prefer = ['main', 'master', 'develop', 'dev']
|
|
8190
|
+
checkoutTarget = prefer.find((n) => realBranches.includes(n)) || realBranches[0]
|
|
8191
|
+
}
|
|
8192
|
+
|
|
8193
|
+
if (checkoutCwd && checkoutTarget) {
|
|
8194
|
+
const checkoutUrl = `/run/scripts/git/checkout.json?cwd=${encodeURIComponent(checkoutCwd)}&commit=${encodeURIComponent(checkoutTarget)}&callback_target=parent&callback=$location.href`
|
|
8195
|
+
returnBtn.addEventListener('click', () => {
|
|
8196
|
+
openCheckoutTerminal(checkoutUrl)
|
|
8197
|
+
})
|
|
8198
|
+
} else {
|
|
8199
|
+
returnBtn.disabled = true
|
|
8200
|
+
if (banner) {
|
|
8201
|
+
banner.classList.add('pinokio-history-latest-banner--disabled')
|
|
8202
|
+
}
|
|
7986
8203
|
}
|
|
7987
8204
|
}
|
|
7988
8205
|
} else if (banner) {
|