pinokiod 3.260.0 → 3.262.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/brew.js +12 -49
- package/kernel/bin/index.js +6 -30
- package/kernel/bin/xcode-tools.js +136 -0
- package/kernel/peer.js +56 -2
- package/package.json +1 -1
- package/server/index.js +0 -4
- package/server/public/common.js +8 -5
- package/server/public/tab-link-popover.css +19 -3
- package/server/public/tab-link-popover.js +27 -12
- package/server/public/urldropdown.css +28 -0
- package/server/public/urldropdown.js +8 -6
- package/server/views/connect/huggingface.ejs +0 -3
- package/server/views/connect/index.ejs +357 -93
- package/server/views/connect.ejs +6 -4
- package/server/views/index.ejs +6 -2
- package/server/views/index2.ejs +4 -2
- package/server/views/net.ejs +9 -4
- package/server/views/network.ejs +4 -2
package/kernel/bin/brew.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
1
2
|
const path = require('path')
|
|
3
|
+
|
|
4
|
+
const { detectCommandLineTools } = require('./xcode-tools')
|
|
2
5
|
class Brew {
|
|
3
6
|
description = "Wait for an install pop-up, then approve."
|
|
4
7
|
async install(req, ondata) {
|
|
@@ -19,64 +22,24 @@ class Brew {
|
|
|
19
22
|
await this.kernel.bin.rm("Homebrew.zip", ondata)
|
|
20
23
|
|
|
21
24
|
if (this.kernel.platform === "darwin") {
|
|
22
|
-
|
|
25
|
+
const checkingMsg = "> checking xcode command line tools...\r\n"
|
|
26
|
+
console.log(checkingMsg)
|
|
27
|
+
ondata({ raw: checkingMsg })
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
ondata(stream)
|
|
29
|
+
const cltStatus = await detectCommandLineTools({
|
|
30
|
+
exec: (params) => this.kernel.bin.exec(params, () => {})
|
|
27
31
|
})
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
const msg =
|
|
32
|
+
|
|
33
|
+
if (cltStatus.valid) {
|
|
34
|
+
const msg = `> command line tools detected at ${cltStatus.path} (pkg ${cltStatus.pkgVersion}, xcode-select ${cltStatus.xcodeSelectVersion}). skipping...\r\n`
|
|
31
35
|
console.log(msg)
|
|
32
36
|
ondata({ raw: msg })
|
|
33
|
-
// check the version.
|
|
34
|
-
// if it's not valid, install the latest
|
|
35
|
-
// if it's valid, skip
|
|
36
|
-
let e4;
|
|
37
|
-
let result = await this.kernel.bin.exec({ message: "xcode-select --version" }, (stream) => {
|
|
38
|
-
ondata(stream)
|
|
39
|
-
})
|
|
40
|
-
if (result && result.stdout) {
|
|
41
|
-
e4 = /xcode-select version ([0-9]+)/gi.exec(result.stdout)
|
|
42
|
-
if (e4.length > 1) {
|
|
43
|
-
let version = Number(e4[1])
|
|
44
|
-
console.log("xcode-select version", version)
|
|
45
|
-
if (version >= 2349) {
|
|
46
|
-
e4 = true
|
|
47
|
-
} else {
|
|
48
|
-
e4 = false
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
e4 = false
|
|
52
|
-
}
|
|
53
|
-
} else {
|
|
54
|
-
e4 = false
|
|
55
|
-
}
|
|
56
|
-
console.log("> e4", e4)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// valid version installed => skip
|
|
60
|
-
if (e4) {
|
|
61
|
-
const msg = "> a valid version command line tools already installed. skipping...\r\n"
|
|
62
|
-
console.log(msg)
|
|
63
|
-
ondata({ raw: msg })
|
|
64
|
-
} else {
|
|
65
|
-
const msg = "> valid version command line tools NOT installed.\r\n"
|
|
66
|
-
console.log(msg)
|
|
67
|
-
ondata({ raw: msg })
|
|
68
|
-
await this._install(req, ondata)
|
|
69
|
-
}
|
|
70
37
|
} else {
|
|
71
|
-
|
|
72
|
-
const msg = "> command line tools not installed yet. install the latest xcode build tools...\r\n"
|
|
38
|
+
const msg = `> ${cltStatus.reason || "command line tools not installed yet."} install the latest xcode build tools...\r\n`
|
|
73
39
|
console.log(msg)
|
|
74
40
|
ondata({ raw: msg })
|
|
75
41
|
await this._install(req, ondata)
|
|
76
42
|
}
|
|
77
|
-
|
|
78
|
-
//ondata({ raw: "Setting CommandLineTools path...\r\n" })
|
|
79
|
-
//await this.kernel.bin.exec({ sudo: true, message: "xcode-select -switch /Library/Developer/CommandLineTools" }, (stream) => { ondata(stream) })
|
|
80
43
|
}
|
|
81
44
|
//
|
|
82
45
|
ondata({ raw: "installing gettext\r\n" })
|
package/kernel/bin/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const LLVM = require('./llvm')
|
|
|
21
21
|
const VS = require("./vs")
|
|
22
22
|
const Cuda = require("./cuda")
|
|
23
23
|
const Torch = require("./torch")
|
|
24
|
+
const { detectCommandLineTools } = require('./xcode-tools')
|
|
24
25
|
const { glob } = require('glob')
|
|
25
26
|
const fakeUa = require('fake-useragent');
|
|
26
27
|
const fse = require('fs-extra')
|
|
@@ -461,36 +462,11 @@ class Bin {
|
|
|
461
462
|
|
|
462
463
|
// check brew_installed
|
|
463
464
|
let e = await this.kernel.bin.exists("homebrew")
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
// - if version is greater thatn 2349 => yes
|
|
470
|
-
// - if version lower than 2349 => no
|
|
471
|
-
// if xcode-select version doesn't match
|
|
472
|
-
// - no
|
|
473
|
-
|
|
474
|
-
let e4;
|
|
475
|
-
let result = await this.exec({ message: "xcode-select --version", conda: { skip: true } }, (stream) => { })
|
|
476
|
-
if (result && result.stdout) {
|
|
477
|
-
e4 = /xcode-select version ([0-9]+)/gi.exec(result.stdout)
|
|
478
|
-
if (e4 && e4.length > 1) {
|
|
479
|
-
let version = Number(e4[1])
|
|
480
|
-
// console.log("xcode-select version", version)
|
|
481
|
-
if (version >= 2349) {
|
|
482
|
-
e4 = true
|
|
483
|
-
} else {
|
|
484
|
-
e4 = false
|
|
485
|
-
}
|
|
486
|
-
} else {
|
|
487
|
-
e4 = false
|
|
488
|
-
}
|
|
489
|
-
} else {
|
|
490
|
-
e4 = false
|
|
491
|
-
}
|
|
492
|
-
console.log("BREW CHECK", { e, e2, e3, e4 })
|
|
493
|
-
this.brew_installed = e && e2 && e3 && e4
|
|
465
|
+
const cltStatus = await detectCommandLineTools({
|
|
466
|
+
exec: (params) => this.exec(params, () => {})
|
|
467
|
+
})
|
|
468
|
+
console.log("BREW CHECK", { homebrew: e, cltStatus })
|
|
469
|
+
this.brew_installed = e && cltStatus.valid
|
|
494
470
|
|
|
495
471
|
}
|
|
496
472
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
const MIN_XCODESELECT_VERSION = 2349
|
|
5
|
+
const REQUIRED_BINARIES = [
|
|
6
|
+
["usr", "bin", "clang"],
|
|
7
|
+
["usr", "bin", "git"]
|
|
8
|
+
]
|
|
9
|
+
const CLT_PACKAGE_IDS = [
|
|
10
|
+
"com.apple.pkg.CLTools_Executables",
|
|
11
|
+
"com.apple.pkg.DeveloperToolsCLI"
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
async function detectCommandLineTools({ exec }) {
|
|
15
|
+
if (typeof exec !== 'function') {
|
|
16
|
+
throw new Error('detectCommandLineTools requires an exec function')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const run = (message) => exec({ message, conda: { skip: true } })
|
|
20
|
+
|
|
21
|
+
const status = { valid: false }
|
|
22
|
+
let pathResult
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
pathResult = await run('xcode-select -p')
|
|
26
|
+
} catch (err) {
|
|
27
|
+
status.reason = 'xcode-select -p failed'
|
|
28
|
+
return status
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const developerPath = extractDeveloperPath(pathResult && pathResult.stdout)
|
|
32
|
+
if (!developerPath) {
|
|
33
|
+
status.rawPathOutput = pathResult ? pathResult.stdout : ''
|
|
34
|
+
status.reason = 'unable to parse developer path from xcode-select output'
|
|
35
|
+
return status
|
|
36
|
+
}
|
|
37
|
+
status.path = developerPath
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const stat = await fs.promises.stat(developerPath)
|
|
41
|
+
if (!stat.isDirectory()) {
|
|
42
|
+
status.reason = `${developerPath} is not a directory`
|
|
43
|
+
return status
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
status.reason = `developer path ${developerPath} is not accessible`
|
|
47
|
+
return status
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
for (const rel of REQUIRED_BINARIES) {
|
|
52
|
+
const binaryPath = path.join(developerPath, ...rel)
|
|
53
|
+
await fs.promises.access(binaryPath, fs.constants.X_OK)
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
status.reason = 'required developer binaries are missing'
|
|
57
|
+
return status
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const pkgInfo = await readCommandLineToolsPkgVersion(run)
|
|
61
|
+
if (!pkgInfo) {
|
|
62
|
+
status.reason = 'unable to read command line tools package info'
|
|
63
|
+
return status
|
|
64
|
+
}
|
|
65
|
+
status.pkgVersion = pkgInfo.version
|
|
66
|
+
|
|
67
|
+
const selectInfo = await readXcodeSelectVersion(run)
|
|
68
|
+
status.xcodeSelectVersion = selectInfo.version
|
|
69
|
+
if (!selectInfo.valid) {
|
|
70
|
+
status.reason = selectInfo.reason || 'xcode-select version below minimum'
|
|
71
|
+
return status
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
status.valid = true
|
|
75
|
+
return status
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function readCommandLineToolsPkgVersion(exec) {
|
|
79
|
+
for (const pkgId of CLT_PACKAGE_IDS) {
|
|
80
|
+
try {
|
|
81
|
+
const result = await exec(`pkgutil --pkg-info=${pkgId}`)
|
|
82
|
+
if (result && result.stdout) {
|
|
83
|
+
const match = /version:\s*([^\n]+)/i.exec(result.stdout)
|
|
84
|
+
if (match) {
|
|
85
|
+
return { pkgId, version: match[1].trim() }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// pkg not installed, try next id
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function readXcodeSelectVersion(exec) {
|
|
96
|
+
let result
|
|
97
|
+
try {
|
|
98
|
+
result = await exec('xcode-select --version')
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return { valid: false, reason: 'xcode-select --version failed' }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const match = result && result.stdout && /xcode-select version\s+(\d+)/i.exec(result.stdout)
|
|
104
|
+
if (!match) {
|
|
105
|
+
return { valid: false, reason: 'unable to parse xcode-select version' }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const numericVersion = Number(match[1])
|
|
109
|
+
return {
|
|
110
|
+
valid: numericVersion >= MIN_XCODESELECT_VERSION,
|
|
111
|
+
version: numericVersion
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
detectCommandLineTools,
|
|
117
|
+
MIN_XCODESELECT_VERSION
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function extractDeveloperPath(stdout) {
|
|
121
|
+
if (!stdout) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const lines = stdout.split(/\r?\n/)
|
|
126
|
+
for (const raw of lines) {
|
|
127
|
+
const line = raw.trim()
|
|
128
|
+
if (!line) {
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if (line.startsWith('/')) {
|
|
132
|
+
return line
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null
|
|
136
|
+
}
|
package/kernel/peer.js
CHANGED
|
@@ -37,8 +37,16 @@ class PeerDiscovery {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
announce() {
|
|
40
|
-
if (this.socket) {
|
|
41
|
-
|
|
40
|
+
if (!this.socket) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
const targets = this._broadcastTargets()
|
|
44
|
+
for (const target of targets) {
|
|
45
|
+
try {
|
|
46
|
+
this.socket.send(this.message, 0, this.message.length, this.port, target)
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('peer broadcast failed', { target, err })
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
async check(kernel) {
|
|
@@ -641,6 +649,7 @@ class PeerDiscovery {
|
|
|
641
649
|
const classification = this.classifyAddress(address, Boolean(iface.internal))
|
|
642
650
|
results.push({
|
|
643
651
|
address,
|
|
652
|
+
netmask: String(iface.netmask || '').trim() || null,
|
|
644
653
|
interface: ifaceName,
|
|
645
654
|
internal: Boolean(iface.internal),
|
|
646
655
|
scope: classification.scope,
|
|
@@ -731,6 +740,51 @@ class PeerDiscovery {
|
|
|
731
740
|
}
|
|
732
741
|
return entries
|
|
733
742
|
}
|
|
743
|
+
_broadcastTargets() {
|
|
744
|
+
const addresses = this._collectInterfaceAddresses()
|
|
745
|
+
this.interface_addresses = addresses
|
|
746
|
+
const targets = new Set()
|
|
747
|
+
for (const entry of addresses) {
|
|
748
|
+
if (!entry || !entry.shareable) {
|
|
749
|
+
continue
|
|
750
|
+
}
|
|
751
|
+
const broadcast = this._deriveBroadcastAddress(entry.address, entry.netmask)
|
|
752
|
+
if (broadcast) {
|
|
753
|
+
targets.add(broadcast)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
targets.add('255.255.255.255')
|
|
757
|
+
return Array.from(targets)
|
|
758
|
+
}
|
|
759
|
+
_deriveBroadcastAddress(address, netmask) {
|
|
760
|
+
const addrOctets = this._parseIPv4(address)
|
|
761
|
+
if (!addrOctets) {
|
|
762
|
+
return null
|
|
763
|
+
}
|
|
764
|
+
let maskOctets = this._parseIPv4(netmask)
|
|
765
|
+
if (!maskOctets) {
|
|
766
|
+
maskOctets = [255, 255, 255, 0]
|
|
767
|
+
}
|
|
768
|
+
const broadcastOctets = addrOctets.map((octet, idx) => {
|
|
769
|
+
const mask = maskOctets[idx]
|
|
770
|
+
return ((octet & mask) | (~mask & 255)) & 255
|
|
771
|
+
})
|
|
772
|
+
const candidate = broadcastOctets.join('.')
|
|
773
|
+
if (candidate.startsWith('127.') || candidate.startsWith('169.254.') || candidate === '0.0.0.0') {
|
|
774
|
+
return null
|
|
775
|
+
}
|
|
776
|
+
return candidate
|
|
777
|
+
}
|
|
778
|
+
_parseIPv4(value) {
|
|
779
|
+
if (!value || typeof value !== 'string') {
|
|
780
|
+
return null
|
|
781
|
+
}
|
|
782
|
+
const octets = value.split('.').map(Number)
|
|
783
|
+
if (octets.length !== 4 || octets.some((val) => Number.isNaN(val) || val < 0 || val > 255)) {
|
|
784
|
+
return null
|
|
785
|
+
}
|
|
786
|
+
return octets
|
|
787
|
+
}
|
|
734
788
|
}
|
|
735
789
|
|
|
736
790
|
module.exports = PeerDiscovery;
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -3250,7 +3250,6 @@ class Server {
|
|
|
3250
3250
|
this.kernel.store.set("theme", config.theme)
|
|
3251
3251
|
//this.theme = config.theme
|
|
3252
3252
|
}
|
|
3253
|
-
console.log("THEME CHANGED", theme_changed)
|
|
3254
3253
|
// 2. Handle HOME
|
|
3255
3254
|
if (config.home) {
|
|
3256
3255
|
// set "new_home"
|
|
@@ -3335,9 +3334,7 @@ class Server {
|
|
|
3335
3334
|
this.kernel.store.set("NO_PROXY", config.NO_PROXY)
|
|
3336
3335
|
|
|
3337
3336
|
if (theme_changed) {
|
|
3338
|
-
console.log("> syncConfig")
|
|
3339
3337
|
await this.syncConfig()
|
|
3340
|
-
console.log("onrefresh", this.onrefresh)
|
|
3341
3338
|
if (this.onrefresh) {
|
|
3342
3339
|
try {
|
|
3343
3340
|
this.onrefresh({ theme: this.theme, colors: this.colors })
|
|
@@ -8208,7 +8205,6 @@ class Server {
|
|
|
8208
8205
|
|
|
8209
8206
|
this.app.post("/config", ex(async (req, res) => {
|
|
8210
8207
|
try {
|
|
8211
|
-
console.log("POST /config", req.body)
|
|
8212
8208
|
let message = await this.setConfig(req.body)
|
|
8213
8209
|
res.json({ success: true, message })
|
|
8214
8210
|
} catch (e) {
|
package/server/public/common.js
CHANGED
|
@@ -2932,6 +2932,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
2932
2932
|
header.appendChild(iconWrapper);
|
|
2933
2933
|
header.appendChild(headingStack);
|
|
2934
2934
|
|
|
2935
|
+
const closeButton = document.createElement('button');
|
|
2936
|
+
closeButton.type = 'button';
|
|
2937
|
+
closeButton.className = 'create-launcher-modal-close';
|
|
2938
|
+
closeButton.setAttribute('aria-label', 'Close create launcher modal');
|
|
2939
|
+
closeButton.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
|
2940
|
+
header.appendChild(closeButton);
|
|
2941
|
+
|
|
2935
2942
|
const promptLabel = document.createElement('label');
|
|
2936
2943
|
promptLabel.className = 'create-launcher-modal-label';
|
|
2937
2944
|
promptLabel.textContent = 'What do you want to do?';
|
|
@@ -3190,12 +3197,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
3190
3197
|
});
|
|
3191
3198
|
|
|
3192
3199
|
cancelButton.addEventListener('click', hideCreateLauncherModal);
|
|
3200
|
+
closeButton.addEventListener('click', hideCreateLauncherModal);
|
|
3193
3201
|
confirmButton.addEventListener('click', submitCreateLauncherModal);
|
|
3194
|
-
overlay.addEventListener('click', (event) => {
|
|
3195
|
-
if (event.target === overlay) {
|
|
3196
|
-
hideCreateLauncherModal();
|
|
3197
|
-
}
|
|
3198
|
-
});
|
|
3199
3202
|
|
|
3200
3203
|
advancedLink.addEventListener('click', () => {
|
|
3201
3204
|
hideCreateLauncherModal();
|
|
@@ -52,9 +52,25 @@ body.dark .tab-link-popover .tab-link-popover-header {
|
|
|
52
52
|
background: transparent;
|
|
53
53
|
cursor: pointer;
|
|
54
54
|
}
|
|
55
|
-
.tab-link-popover .tab-link-popover-item.qr-inline {
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
.tab-link-popover .tab-link-popover-item.qr-inline {
|
|
56
|
+
flex-direction: row;
|
|
57
|
+
align-items: flex-start;
|
|
58
|
+
gap: 12px;
|
|
59
|
+
}
|
|
60
|
+
.tab-link-popover .tab-link-popover-item.qr-inline .textcol {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 2px;
|
|
64
|
+
min-width: 0;
|
|
65
|
+
flex: 1 1 auto;
|
|
66
|
+
}
|
|
67
|
+
.tab-link-popover .tab-link-popover-item.qr-inline .qr {
|
|
68
|
+
width: 128px;
|
|
69
|
+
height: 128px;
|
|
70
|
+
image-rendering: pixelated;
|
|
71
|
+
flex: 0 0 auto;
|
|
72
|
+
margin-left: auto;
|
|
73
|
+
}
|
|
58
74
|
.tab-link-popover .tab-link-popover-item:hover,
|
|
59
75
|
.tab-link-popover .tab-link-popover-item:focus-visible {
|
|
60
76
|
background: rgba(15, 23, 42, 0.06);
|
|
@@ -272,8 +272,8 @@
|
|
|
272
272
|
const parsed = new URL(value, location.origin)
|
|
273
273
|
const host = parsed.host
|
|
274
274
|
const pathname = parsed.pathname || "/"
|
|
275
|
-
const
|
|
276
|
-
return `${host}${pathname}${
|
|
275
|
+
const hash = parsed.hash || ""
|
|
276
|
+
return `${host}${pathname}${hash}`
|
|
277
277
|
} catch (_) {
|
|
278
278
|
return value
|
|
279
279
|
}
|
|
@@ -1237,22 +1237,32 @@
|
|
|
1237
1237
|
popover.style.display = "flex"
|
|
1238
1238
|
popover.classList.add("visible")
|
|
1239
1239
|
popover.style.visibility = "hidden"
|
|
1240
|
+
popover.style.maxHeight = ""
|
|
1241
|
+
popover.style.overflowY = ""
|
|
1240
1242
|
|
|
1241
1243
|
const popoverWidth = popover.offsetWidth
|
|
1242
|
-
|
|
1244
|
+
let popoverHeight = popover.offsetHeight
|
|
1245
|
+
const viewportPadding = 12
|
|
1246
|
+
const availableHeight = Math.max(80, window.innerHeight - viewportPadding * 2)
|
|
1247
|
+
|
|
1248
|
+
if (popoverHeight > availableHeight) {
|
|
1249
|
+
popover.style.maxHeight = `${Math.round(availableHeight)}px`
|
|
1250
|
+
popover.style.overflowY = "auto"
|
|
1251
|
+
popoverHeight = Math.min(availableHeight, popover.offsetHeight)
|
|
1252
|
+
}
|
|
1243
1253
|
|
|
1244
1254
|
let left = rect.left
|
|
1245
1255
|
let top = rect.bottom + 8
|
|
1246
1256
|
|
|
1247
|
-
if (left + popoverWidth > window.innerWidth -
|
|
1248
|
-
left = window.innerWidth - popoverWidth -
|
|
1257
|
+
if (left + popoverWidth > window.innerWidth - viewportPadding) {
|
|
1258
|
+
left = window.innerWidth - popoverWidth - viewportPadding
|
|
1249
1259
|
}
|
|
1250
|
-
if (left <
|
|
1251
|
-
left =
|
|
1260
|
+
if (left < viewportPadding) {
|
|
1261
|
+
left = viewportPadding
|
|
1252
1262
|
}
|
|
1253
1263
|
|
|
1254
|
-
if (top + popoverHeight > window.innerHeight -
|
|
1255
|
-
top = Math.max(
|
|
1264
|
+
if (top + popoverHeight > window.innerHeight - viewportPadding) {
|
|
1265
|
+
top = Math.max(viewportPadding, rect.top - popoverHeight - 8)
|
|
1256
1266
|
}
|
|
1257
1267
|
|
|
1258
1268
|
popover.style.left = `${Math.round(left)}px`
|
|
@@ -1521,6 +1531,7 @@
|
|
|
1521
1531
|
const valueSpan = document.createElement("span")
|
|
1522
1532
|
valueSpan.className = "value"
|
|
1523
1533
|
valueSpan.textContent = entry.display
|
|
1534
|
+
valueSpan.title = entry.url
|
|
1524
1535
|
|
|
1525
1536
|
if (entry.type === 'http' && entry.qr === true) {
|
|
1526
1537
|
item.className = "tab-link-popover-item qr-inline"
|
|
@@ -1608,10 +1619,14 @@
|
|
|
1608
1619
|
hideTabLinkPopover({ immediate: true })
|
|
1609
1620
|
}
|
|
1610
1621
|
|
|
1611
|
-
window.addEventListener("scroll", () => {
|
|
1612
|
-
if (tabLinkPopoverEl
|
|
1613
|
-
|
|
1622
|
+
window.addEventListener("scroll", (event) => {
|
|
1623
|
+
if (!tabLinkPopoverEl || !tabLinkPopoverEl.classList.contains("visible")) {
|
|
1624
|
+
return
|
|
1625
|
+
}
|
|
1626
|
+
if (event && event.target && tabLinkPopoverEl.contains(event.target)) {
|
|
1627
|
+
return
|
|
1614
1628
|
}
|
|
1629
|
+
hideTabLinkPopover({ immediate: true })
|
|
1615
1630
|
}, true)
|
|
1616
1631
|
|
|
1617
1632
|
window.addEventListener("resize", () => {
|
|
@@ -200,6 +200,7 @@ body.dark .url-dropdown-empty-description {
|
|
|
200
200
|
font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
201
201
|
-webkit-backdrop-filter: blur(28px);
|
|
202
202
|
backdrop-filter: blur(28px);
|
|
203
|
+
position: relative;
|
|
203
204
|
}
|
|
204
205
|
body.dark .create-launcher-modal {
|
|
205
206
|
background: rgba(17, 24, 39, 0.82);
|
|
@@ -215,6 +216,33 @@ body.dark .create-launcher-modal {
|
|
|
215
216
|
align-items: center;
|
|
216
217
|
gap: 18px;
|
|
217
218
|
padding-bottom: 4px;
|
|
219
|
+
position: relative;
|
|
220
|
+
}
|
|
221
|
+
.create-launcher-modal-close {
|
|
222
|
+
position: absolute;
|
|
223
|
+
top: 0;
|
|
224
|
+
right: 0;
|
|
225
|
+
border: none;
|
|
226
|
+
background: transparent;
|
|
227
|
+
color: #6b7280;
|
|
228
|
+
font-size: 18px;
|
|
229
|
+
padding: 6px;
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
line-height: 1;
|
|
232
|
+
}
|
|
233
|
+
.create-launcher-modal-close:hover {
|
|
234
|
+
color: #4f46e5;
|
|
235
|
+
}
|
|
236
|
+
.create-launcher-modal-close:focus-visible {
|
|
237
|
+
outline: 2px solid #4f46e5;
|
|
238
|
+
outline-offset: 2px;
|
|
239
|
+
border-radius: 4px;
|
|
240
|
+
}
|
|
241
|
+
body.dark .create-launcher-modal-close {
|
|
242
|
+
color: rgba(255,255,255,0.75);
|
|
243
|
+
}
|
|
244
|
+
body.dark .create-launcher-modal-close:hover {
|
|
245
|
+
color: #a5b4fc;
|
|
218
246
|
}
|
|
219
247
|
.create-launcher-modal-icon {
|
|
220
248
|
width: 44px;
|
|
@@ -1163,6 +1163,12 @@ function initUrlDropdown(config = {}) {
|
|
|
1163
1163
|
modalContent.setAttribute('role', 'dialog');
|
|
1164
1164
|
modalContent.setAttribute('aria-modal', 'true');
|
|
1165
1165
|
|
|
1166
|
+
const closeButton = document.createElement('button');
|
|
1167
|
+
closeButton.type = 'button';
|
|
1168
|
+
closeButton.className = 'create-launcher-modal-close';
|
|
1169
|
+
closeButton.setAttribute('aria-label', 'Close create launcher modal');
|
|
1170
|
+
closeButton.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
|
1171
|
+
|
|
1166
1172
|
const title = document.createElement('h3');
|
|
1167
1173
|
title.id = 'quick-create-launcher-title';
|
|
1168
1174
|
title.textContent = 'Create';
|
|
@@ -1204,6 +1210,7 @@ function initUrlDropdown(config = {}) {
|
|
|
1204
1210
|
actions.appendChild(confirmButton);
|
|
1205
1211
|
|
|
1206
1212
|
label.appendChild(input);
|
|
1213
|
+
modalContent.appendChild(closeButton);
|
|
1207
1214
|
modalContent.appendChild(title);
|
|
1208
1215
|
modalContent.appendChild(description);
|
|
1209
1216
|
modalContent.appendChild(label);
|
|
@@ -1212,13 +1219,8 @@ function initUrlDropdown(config = {}) {
|
|
|
1212
1219
|
overlay.appendChild(modalContent);
|
|
1213
1220
|
document.body.appendChild(overlay);
|
|
1214
1221
|
|
|
1215
|
-
overlay.addEventListener('click', function(event) {
|
|
1216
|
-
if (event.target === overlay) {
|
|
1217
|
-
hideCreateLauncherModal();
|
|
1218
|
-
}
|
|
1219
|
-
});
|
|
1220
|
-
|
|
1221
1222
|
cancelButton.addEventListener('click', hideCreateLauncherModal);
|
|
1223
|
+
closeButton.addEventListener('click', hideCreateLauncherModal);
|
|
1222
1224
|
confirmButton.addEventListener('click', confirmCreateLauncherModal);
|
|
1223
1225
|
input.addEventListener('keydown', handleCreateModalKeydown);
|
|
1224
1226
|
|
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title><%=name%> OAuth</title>
|
|
7
|
+
<link href="/xterm.min.css" rel="stylesheet" />
|
|
7
8
|
<link href="/css/fontawesome.min.css" rel="stylesheet">
|
|
8
9
|
<link href="/css/solid.min.css" rel="stylesheet">
|
|
9
10
|
<link href="/css/regular.min.css" rel="stylesheet">
|
|
10
11
|
<link href="/css/brands.min.css" rel="stylesheet">
|
|
12
|
+
<link href="/markdown.css" rel="stylesheet"/>
|
|
13
|
+
<link href="/noty.css" rel="stylesheet"/>
|
|
14
|
+
<link href="/style.css" rel="stylesheet"/>
|
|
15
|
+
<% if (agent === "electron") { %>
|
|
16
|
+
<link href="/electron.css" rel="stylesheet"/>
|
|
17
|
+
<% } %>
|
|
11
18
|
<style>
|
|
12
19
|
body {
|
|
13
20
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
14
|
-
max-width: 600px;
|
|
15
|
-
margin: 50px auto;
|
|
16
|
-
padding: 20px;
|
|
17
21
|
background: #f8fafc;
|
|
18
22
|
}
|
|
19
23
|
/*
|
|
@@ -24,6 +28,14 @@
|
|
|
24
28
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
25
29
|
}
|
|
26
30
|
*/
|
|
31
|
+
.container {
|
|
32
|
+
background: white;
|
|
33
|
+
max-width: 600px;
|
|
34
|
+
margin: 0 auto;
|
|
35
|
+
padding: 30px;
|
|
36
|
+
border-radius: 10px;
|
|
37
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
38
|
+
}
|
|
27
39
|
h1 {
|
|
28
40
|
text-transform: capitalize;
|
|
29
41
|
color: #1f2937;
|
|
@@ -79,6 +91,47 @@
|
|
|
79
91
|
.status.error { background: #fee2e2; color: #991b1b; }
|
|
80
92
|
.status.warning { background: #fef3c7; color: #92400e; }
|
|
81
93
|
.hidden { display: none; }
|
|
94
|
+
.loader-overlay {
|
|
95
|
+
position: fixed;
|
|
96
|
+
top: 0;
|
|
97
|
+
left: 0;
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: 100%;
|
|
100
|
+
background: rgba(248, 250, 252, 0.95);
|
|
101
|
+
display: flex;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
gap: 16px;
|
|
106
|
+
z-index: 999;
|
|
107
|
+
}
|
|
108
|
+
.loader-overlay.hidden {
|
|
109
|
+
display: none;
|
|
110
|
+
}
|
|
111
|
+
.loader-spinner {
|
|
112
|
+
width: 48px;
|
|
113
|
+
height: 48px;
|
|
114
|
+
border: 4px solid #e5e7eb;
|
|
115
|
+
border-top: 4px solid #ff6b35;
|
|
116
|
+
border-radius: 50%;
|
|
117
|
+
animation: spin 1s linear infinite;
|
|
118
|
+
}
|
|
119
|
+
@keyframes spin {
|
|
120
|
+
to { transform: rotate(360deg); }
|
|
121
|
+
}
|
|
122
|
+
.loader-message {
|
|
123
|
+
font-size: 16px;
|
|
124
|
+
color: #374151;
|
|
125
|
+
text-align: center;
|
|
126
|
+
}
|
|
127
|
+
.loader-cancel {
|
|
128
|
+
background: transparent;
|
|
129
|
+
border: 1px solid #6b7280;
|
|
130
|
+
color: #374151;
|
|
131
|
+
padding: 8px 16px;
|
|
132
|
+
border-radius: 6px;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
}
|
|
82
135
|
.profile {
|
|
83
136
|
width: 400px;
|
|
84
137
|
display: flex;
|
|
@@ -96,9 +149,17 @@
|
|
|
96
149
|
}
|
|
97
150
|
header {
|
|
98
151
|
text-align: center;
|
|
99
|
-
padding: 50px;
|
|
100
152
|
letter-spacing: -1px;
|
|
101
153
|
}
|
|
154
|
+
header.head {
|
|
155
|
+
max-width: 600px;
|
|
156
|
+
margin: 0 auto;
|
|
157
|
+
padding: 50px;
|
|
158
|
+
text-align: center;
|
|
159
|
+
}
|
|
160
|
+
header.head h1 {
|
|
161
|
+
justify-content: center;
|
|
162
|
+
}
|
|
102
163
|
.logos {
|
|
103
164
|
display: flex;
|
|
104
165
|
justify-content: center;
|
|
@@ -118,9 +179,40 @@
|
|
|
118
179
|
font-size: 14px;
|
|
119
180
|
}
|
|
120
181
|
</style>
|
|
182
|
+
<script src="/popper.min.js"></script>
|
|
183
|
+
<script src="/tippy-bundle.umd.min.js"></script>
|
|
184
|
+
<script src="/hotkeys.min.js"></script>
|
|
185
|
+
<script src="/sweetalert2.js"></script>
|
|
186
|
+
<script src="/nav.js"></script>
|
|
121
187
|
</head>
|
|
122
|
-
<body>
|
|
123
|
-
<header>
|
|
188
|
+
<body class='<%=theme%>' data-agent="<%=agent%>">
|
|
189
|
+
<header class="navheader grabbable">
|
|
190
|
+
<h1>
|
|
191
|
+
<a class="home" href="/home"><img class="icon" src="/pinokio-black.png"></a>
|
|
192
|
+
<button class="btn2" id="minimize-header" data-tippy-content="fullscreen" title="fullscreen">
|
|
193
|
+
<div><i class="fa-solid fa-expand"></i></div>
|
|
194
|
+
</button>
|
|
195
|
+
<button class="btn2" id="back" data-tippy-content="back"><div><i class="fa-solid fa-chevron-left"></i></div></button>
|
|
196
|
+
<button class="btn2" id="forward" data-tippy-content="forward"><div><i class="fa-solid fa-chevron-right"></i></div></button>
|
|
197
|
+
<button class="btn2" id="refresh-page" data-tippy-content="refresh"><div><i class="fa-solid fa-rotate-right"></i></div></button>
|
|
198
|
+
<button class="btn2" id="screenshot" data-tippy-content="screen capture"><i class="fa-solid fa-camera"></i></button>
|
|
199
|
+
<button class="btn2" id="inspector" data-tippy-content="X-ray mode"><i class="fa-solid fa-eye"></i></button>
|
|
200
|
+
<div class="flexible"></div>
|
|
201
|
+
<a class="btn2" href="/columns" data-tippy-content="split into 2 columns">
|
|
202
|
+
<div><i class="fa-solid fa-table-columns"></i></div>
|
|
203
|
+
</a>
|
|
204
|
+
<a class="btn2" href="/rows" data-tippy-content="split into 2 rows">
|
|
205
|
+
<div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
|
|
206
|
+
</a>
|
|
207
|
+
<button class="btn2" id="new-window" data-tippy-content="open a new window" title="open a new window" data-agent="web">
|
|
208
|
+
<div><i class="fa-solid fa-plus"></i></div>
|
|
209
|
+
</button>
|
|
210
|
+
<button class="btn2 hidden" id="close-window" data-tippy-content="close this section">
|
|
211
|
+
<div><i class="fa-solid fa-xmark"></i></div>
|
|
212
|
+
</button>
|
|
213
|
+
</h1>
|
|
214
|
+
</header>
|
|
215
|
+
<header class='head'>
|
|
124
216
|
<div class='logos'>
|
|
125
217
|
<img class='logo' src="/pinokio-black.png"/>
|
|
126
218
|
<div>+</div>
|
|
@@ -135,31 +227,196 @@
|
|
|
135
227
|
<h1><%=name%> Connect</h1>
|
|
136
228
|
</header>
|
|
137
229
|
<div class="container">
|
|
230
|
+
<div id="status" class="status <%= protocol === "https" ? "warning" : "hidden" %>">
|
|
231
|
+
<% if (protocol === "https") { %>
|
|
232
|
+
Checking authentication status...
|
|
233
|
+
<% } %>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
138
236
|
<% if (protocol === "https") { %>
|
|
139
|
-
<div id="status" class="status warning">
|
|
140
|
-
Checking authentication status...
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
<!-- Login Section -->
|
|
144
237
|
<div id="login-section">
|
|
145
238
|
<p>Click below to authenticate with <%=name%>:</p>
|
|
146
239
|
<button class="btn" onclick="login()">Login</button>
|
|
147
240
|
</div>
|
|
148
|
-
|
|
149
|
-
<!-- User Section -->
|
|
150
|
-
<div id="user-section" class="hidden">
|
|
151
|
-
<div class="user-info">
|
|
152
|
-
<h3>Logged In</h3>
|
|
153
|
-
<div id="user-details"></div>
|
|
154
|
-
<button class="btn secondary" onclick="logout()">Logout</button>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
241
|
<% } else { %>
|
|
158
|
-
<
|
|
242
|
+
<div id="http-section">
|
|
243
|
+
<p>Click below to open the secure connection flow:</p>
|
|
244
|
+
<button class='btn' id="get-started-button">Get started</button>
|
|
245
|
+
</div>
|
|
159
246
|
<% } %>
|
|
160
|
-
|
|
247
|
+
|
|
248
|
+
<div id="user-section" class="hidden">
|
|
249
|
+
<div class="user-info">
|
|
250
|
+
<h3>Logged In</h3>
|
|
251
|
+
<div id="user-details"></div>
|
|
252
|
+
<button class="btn secondary" onclick="logout()">Logout</button>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
161
256
|
</div>
|
|
162
257
|
|
|
258
|
+
<div id="connect-loader" class="loader-overlay hidden">
|
|
259
|
+
<div class="loader-spinner"></div>
|
|
260
|
+
<div id="loader-message" class="loader-message">Connecting...</div>
|
|
261
|
+
<button id="loader-cancel" class="loader-cancel hidden">Cancel</button>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<script>
|
|
265
|
+
const CONNECT_NAME = "<%=name%>"
|
|
266
|
+
const statusElement = document.getElementById('status')
|
|
267
|
+
const loaderElement = document.getElementById('connect-loader')
|
|
268
|
+
const loaderMessageElement = document.getElementById('loader-message')
|
|
269
|
+
const loaderCancelButton = document.getElementById('loader-cancel')
|
|
270
|
+
const loginSection = document.getElementById('login-section')
|
|
271
|
+
const httpSection = document.getElementById('http-section')
|
|
272
|
+
const userSection = document.getElementById('user-section')
|
|
273
|
+
const userDetailsElement = document.getElementById('user-details')
|
|
274
|
+
const CONNECT_PROFILE_URL = `/connect/${CONNECT_NAME}/profile`
|
|
275
|
+
const CONNECT_LOGOUT_URL = `/connect/${CONNECT_NAME}/logout`
|
|
276
|
+
let loaderCancelHandler = null
|
|
277
|
+
|
|
278
|
+
function setStatus(message, type = 'warning') {
|
|
279
|
+
if (!statusElement) return
|
|
280
|
+
if (type === 'hidden') {
|
|
281
|
+
statusElement.textContent = message || ''
|
|
282
|
+
statusElement.className = 'status hidden'
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
statusElement.textContent = message
|
|
286
|
+
statusElement.className = `status ${type}`
|
|
287
|
+
statusElement.classList.remove('hidden')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function showLoader(message = 'Connecting...', options = {}) {
|
|
291
|
+
if (!loaderElement) return
|
|
292
|
+
loaderElement.classList.remove('hidden')
|
|
293
|
+
if (loaderMessageElement) {
|
|
294
|
+
loaderMessageElement.textContent = message
|
|
295
|
+
}
|
|
296
|
+
if (loaderCancelButton) {
|
|
297
|
+
if (options.cancellable) {
|
|
298
|
+
loaderCancelHandler = typeof options.onCancel === 'function' ? options.onCancel : null
|
|
299
|
+
loaderCancelButton.classList.remove('hidden')
|
|
300
|
+
loaderCancelButton.onclick = () => {
|
|
301
|
+
if (loaderCancelHandler) {
|
|
302
|
+
loaderCancelHandler()
|
|
303
|
+
} else {
|
|
304
|
+
hideLoader()
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
loaderCancelButton.classList.add('hidden')
|
|
309
|
+
loaderCancelButton.onclick = null
|
|
310
|
+
loaderCancelHandler = null
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function hideLoader() {
|
|
316
|
+
if (!loaderElement) return
|
|
317
|
+
loaderElement.classList.add('hidden')
|
|
318
|
+
if (loaderCancelButton) {
|
|
319
|
+
loaderCancelButton.classList.add('hidden')
|
|
320
|
+
loaderCancelButton.onclick = null
|
|
321
|
+
}
|
|
322
|
+
loaderCancelHandler = null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function ensureValidToken() {
|
|
326
|
+
try {
|
|
327
|
+
const res = await fetch(`/connect/${CONNECT_NAME}/keys`, {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: { 'Content-Type': 'application/json' },
|
|
330
|
+
body: JSON.stringify({})
|
|
331
|
+
})
|
|
332
|
+
const json = await res.json()
|
|
333
|
+
if (json && json.access_token && !json.error) {
|
|
334
|
+
return json.access_token
|
|
335
|
+
}
|
|
336
|
+
return null
|
|
337
|
+
} catch (err) {
|
|
338
|
+
console.error('Failed to check connect status', err)
|
|
339
|
+
return null
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function fetchUserInfo(existingToken) {
|
|
344
|
+
try {
|
|
345
|
+
const token = existingToken || await ensureValidToken()
|
|
346
|
+
if (!token) {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
await displayUserInfo()
|
|
350
|
+
return true
|
|
351
|
+
} catch (err) {
|
|
352
|
+
console.error('Failed to fetch user info', err)
|
|
353
|
+
setStatus('Failed to fetch user info: ' + err.message, 'error')
|
|
354
|
+
hideLoader()
|
|
355
|
+
return false
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function displayUserInfo() {
|
|
360
|
+
if (!userSection || !userDetailsElement) {
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
const res = await fetch(CONNECT_PROFILE_URL)
|
|
364
|
+
if (!res.ok) {
|
|
365
|
+
throw new Error('Profile fetch failed')
|
|
366
|
+
}
|
|
367
|
+
const profile = await res.json()
|
|
368
|
+
const rows = profile.items.map((row) => {
|
|
369
|
+
return `<tr><td>${row.key}</td><td>${row.val}</td></tr>`
|
|
370
|
+
}).join("")
|
|
371
|
+
userDetailsElement.innerHTML = `<div class="profile">
|
|
372
|
+
<img src="${profile.image}">
|
|
373
|
+
<div class='profile-column'>
|
|
374
|
+
<table>${rows}</table>
|
|
375
|
+
</div>
|
|
376
|
+
</div>`
|
|
377
|
+
if (loginSection) {
|
|
378
|
+
loginSection.classList.add('hidden')
|
|
379
|
+
}
|
|
380
|
+
if (httpSection) {
|
|
381
|
+
httpSection.classList.add('hidden')
|
|
382
|
+
}
|
|
383
|
+
userSection.classList.remove('hidden')
|
|
384
|
+
setStatus('', 'hidden')
|
|
385
|
+
hideLoader()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function logout() {
|
|
389
|
+
try {
|
|
390
|
+
showLoader('Disconnecting...', { cancellable: false })
|
|
391
|
+
const res = await fetch(CONNECT_LOGOUT_URL, {
|
|
392
|
+
method: 'POST',
|
|
393
|
+
headers: { 'Content-Type': 'application/json' },
|
|
394
|
+
body: JSON.stringify({})
|
|
395
|
+
})
|
|
396
|
+
await res.json()
|
|
397
|
+
setStatus('Logged out', 'warning')
|
|
398
|
+
} catch (err) {
|
|
399
|
+
console.error('Failed to logout', err)
|
|
400
|
+
setStatus('Failed to logout: ' + err.message, 'error')
|
|
401
|
+
} finally {
|
|
402
|
+
hideLoader()
|
|
403
|
+
if (loginSection) {
|
|
404
|
+
loginSection.classList.remove('hidden')
|
|
405
|
+
}
|
|
406
|
+
if (httpSection) {
|
|
407
|
+
httpSection.classList.remove('hidden')
|
|
408
|
+
}
|
|
409
|
+
if (userSection) {
|
|
410
|
+
userSection.classList.add('hidden')
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
window.ensureValidToken = ensureValidToken
|
|
416
|
+
window.fetchUserInfo = fetchUserInfo
|
|
417
|
+
window.logout = logout
|
|
418
|
+
</script>
|
|
419
|
+
|
|
163
420
|
<% if (protocol === "https") { %>
|
|
164
421
|
<script>
|
|
165
422
|
|
|
@@ -173,13 +430,6 @@
|
|
|
173
430
|
const SCOPE = "<%=config.SCOPE%>"
|
|
174
431
|
const name = "<%=name%>"
|
|
175
432
|
|
|
176
|
-
// Utility functions
|
|
177
|
-
function setStatus(message, type) {
|
|
178
|
-
const status = document.getElementById('status');
|
|
179
|
-
status.textContent = message;
|
|
180
|
-
status.className = `status ${type}`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
433
|
function generateRandomString(length) {
|
|
184
434
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
185
435
|
let result = '';
|
|
@@ -210,25 +460,11 @@
|
|
|
210
460
|
.replace(/=/g, '');
|
|
211
461
|
}
|
|
212
462
|
|
|
213
|
-
// Token management with automatic refresh
|
|
214
|
-
async function ensureValidToken() {
|
|
215
|
-
const res = await fetch(`/connect/${name}/keys`, {
|
|
216
|
-
method: 'POST',
|
|
217
|
-
headers: { 'Content-Type': 'application/json' },
|
|
218
|
-
body: JSON.stringify({})
|
|
219
|
-
});
|
|
220
|
-
const json = await res.json();
|
|
221
|
-
console.log({ json })
|
|
222
|
-
if (json && json.access_token) {
|
|
223
|
-
return json.access_token
|
|
224
|
-
} else {
|
|
225
|
-
return null
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
463
|
// OAuth functions
|
|
230
464
|
async function login() {
|
|
231
465
|
try {
|
|
466
|
+
showLoader('Connecting...');
|
|
467
|
+
setStatus(`Redirecting to ${name}...`, 'warning');
|
|
232
468
|
// Clear existing data
|
|
233
469
|
localStorage.removeItem('oauth_state');
|
|
234
470
|
localStorage.removeItem('code_verifier');
|
|
@@ -260,11 +496,13 @@
|
|
|
260
496
|
|
|
261
497
|
} catch (error) {
|
|
262
498
|
console.error('Login error:', error);
|
|
499
|
+
hideLoader();
|
|
263
500
|
setStatus('Failed to start login', 'error');
|
|
264
501
|
}
|
|
265
502
|
}
|
|
266
503
|
|
|
267
504
|
async function handleOAuthCallback(code) {
|
|
505
|
+
showLoader('Finishing connection...');
|
|
268
506
|
try {
|
|
269
507
|
// Verify state
|
|
270
508
|
const urlParams = new URLSearchParams(window.location.search);
|
|
@@ -303,63 +541,19 @@
|
|
|
303
541
|
|
|
304
542
|
// Get user info
|
|
305
543
|
await fetchUserInfo();
|
|
544
|
+
setStatus('Connection complete. Refreshing...', 'success');
|
|
545
|
+
window.location.reload();
|
|
306
546
|
} else {
|
|
307
547
|
throw new Error('No access token received');
|
|
308
548
|
}
|
|
309
549
|
|
|
310
550
|
} catch (error) {
|
|
311
551
|
console.error('OAuth callback error:', error);
|
|
552
|
+
hideLoader();
|
|
312
553
|
setStatus('Authentication failed: ' + error.message, 'error');
|
|
313
554
|
}
|
|
314
555
|
}
|
|
315
556
|
|
|
316
|
-
async function fetchUserInfo() {
|
|
317
|
-
try {
|
|
318
|
-
// Use ensureValidToken to automatically refresh if needed
|
|
319
|
-
const token = await ensureValidToken();
|
|
320
|
-
if (!token) {
|
|
321
|
-
throw new Error('No valid token available');
|
|
322
|
-
}
|
|
323
|
-
console.log({ token })
|
|
324
|
-
await displayUserInfo()
|
|
325
|
-
} catch (error) {
|
|
326
|
-
console.error('Error fetching user info:', error);
|
|
327
|
-
setStatus('Failed to fetch user info: ' + error.message, 'error');
|
|
328
|
-
// logout();
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function displayUserInfo() {
|
|
333
|
-
const res = await fetch(`/connect/${name}/profile`)
|
|
334
|
-
const profile = await res.json();
|
|
335
|
-
const userDetails = document.getElementById('user-details');
|
|
336
|
-
let rows = profile.items.map((row) => {
|
|
337
|
-
return `<tr><td>${row.key}</td><td>${row.val}</td></tr>`
|
|
338
|
-
}).join("")
|
|
339
|
-
userDetails.innerHTML = `<div class="profile">
|
|
340
|
-
<img src="${profile.image}">
|
|
341
|
-
<div class='profile-column'>
|
|
342
|
-
<table>${rows}</table>
|
|
343
|
-
</div>
|
|
344
|
-
</div>`
|
|
345
|
-
document.getElementById('login-section').className = 'hidden';
|
|
346
|
-
document.getElementById('user-section').className = '';
|
|
347
|
-
setStatus("", "hidden")
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async function logout() {
|
|
351
|
-
document.getElementById('login-section').className = '';
|
|
352
|
-
document.getElementById('user-section').className = 'hidden';
|
|
353
|
-
setStatus('Logged out', 'warning');
|
|
354
|
-
const res = await fetch('/connect/<%=name%>/logout', {
|
|
355
|
-
method: 'POST',
|
|
356
|
-
headers: { 'Content-Type': 'application/json' },
|
|
357
|
-
body: JSON.stringify({})
|
|
358
|
-
});
|
|
359
|
-
const json = await res.json();
|
|
360
|
-
location.href = location.href
|
|
361
|
-
}
|
|
362
|
-
|
|
363
557
|
// Utility function for making authenticated API calls with automatic refresh
|
|
364
558
|
async function makeAuthenticatedRequest(url, options = {}) {
|
|
365
559
|
const token = await ensureValidToken();
|
|
@@ -395,7 +589,7 @@
|
|
|
395
589
|
// Check existing session with automatic refresh
|
|
396
590
|
const token = await ensureValidToken();
|
|
397
591
|
if (token) {
|
|
398
|
-
await fetchUserInfo();
|
|
592
|
+
await fetchUserInfo(token);
|
|
399
593
|
} else {
|
|
400
594
|
setStatus('Not authenticated', 'warning');
|
|
401
595
|
}
|
|
@@ -405,6 +599,76 @@
|
|
|
405
599
|
window.makeAuthenticatedRequest = makeAuthenticatedRequest;
|
|
406
600
|
window.ensureValidToken = ensureValidToken;
|
|
407
601
|
</script>
|
|
602
|
+
<% } else { %>
|
|
603
|
+
<script>
|
|
604
|
+
const SECURE_CONNECT_URL = 'https://pinokio.localhost/connect/<%=name%>'
|
|
605
|
+
const CONNECT_POLL_INTERVAL = 4000
|
|
606
|
+
const CONNECT_TIMEOUT = 120000
|
|
607
|
+
let connectPollTimer = null
|
|
608
|
+
let connectTimeoutHandle = null
|
|
609
|
+
|
|
610
|
+
function stopConnectPolling(message, type = 'warning') {
|
|
611
|
+
if (connectPollTimer) {
|
|
612
|
+
clearInterval(connectPollTimer)
|
|
613
|
+
connectPollTimer = null
|
|
614
|
+
}
|
|
615
|
+
if (connectTimeoutHandle) {
|
|
616
|
+
clearTimeout(connectTimeoutHandle)
|
|
617
|
+
connectTimeoutHandle = null
|
|
618
|
+
}
|
|
619
|
+
hideLoader()
|
|
620
|
+
if (message) {
|
|
621
|
+
setStatus(message, type)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function pollConnectStatus() {
|
|
626
|
+
const token = await ensureValidToken()
|
|
627
|
+
if (token) {
|
|
628
|
+
if (connectPollTimer) {
|
|
629
|
+
clearInterval(connectPollTimer)
|
|
630
|
+
connectPollTimer = null
|
|
631
|
+
}
|
|
632
|
+
if (connectTimeoutHandle) {
|
|
633
|
+
clearTimeout(connectTimeoutHandle)
|
|
634
|
+
connectTimeoutHandle = null
|
|
635
|
+
}
|
|
636
|
+
await fetchUserInfo(token)
|
|
637
|
+
setStatus('Connected.', 'success')
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function startHttpConnect() {
|
|
642
|
+
stopConnectPolling()
|
|
643
|
+
showLoader('Connecting... Complete the login in the secure window.', {
|
|
644
|
+
cancellable: true,
|
|
645
|
+
onCancel: () => stopConnectPolling('Connection cancelled.', 'warning')
|
|
646
|
+
})
|
|
647
|
+
setStatus('Waiting for connection...', 'warning')
|
|
648
|
+
const secureWindow = window.open(SECURE_CONNECT_URL, '_blank')
|
|
649
|
+
if (!secureWindow) {
|
|
650
|
+
window.location.href = SECURE_CONNECT_URL
|
|
651
|
+
return
|
|
652
|
+
}
|
|
653
|
+
pollConnectStatus()
|
|
654
|
+
connectPollTimer = setInterval(pollConnectStatus, CONNECT_POLL_INTERVAL)
|
|
655
|
+
connectTimeoutHandle = setTimeout(() => {
|
|
656
|
+
stopConnectPolling('Timed out waiting for connection. Please try again.', 'error')
|
|
657
|
+
}, CONNECT_TIMEOUT)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const getStartedButton = document.getElementById('get-started-button')
|
|
661
|
+
if (getStartedButton) {
|
|
662
|
+
getStartedButton.addEventListener('click', startHttpConnect)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
window.addEventListener('load', async () => {
|
|
666
|
+
const token = await ensureValidToken()
|
|
667
|
+
if (token) {
|
|
668
|
+
await fetchUserInfo(token)
|
|
669
|
+
}
|
|
670
|
+
})
|
|
671
|
+
</script>
|
|
408
672
|
<% } %>
|
|
409
673
|
</body>
|
|
410
674
|
</html>
|
package/server/views/connect.ejs
CHANGED
|
@@ -322,8 +322,10 @@ body.dark .browser-options-row {
|
|
|
322
322
|
}
|
|
323
323
|
*/
|
|
324
324
|
body.dark .context-menu-wrapper {
|
|
325
|
-
background: rgba(0,0,0,0.9);
|
|
326
|
-
color: white;
|
|
325
|
+
background: rgba(0,0,0,0.9) !important;
|
|
326
|
+
color: white !important;
|
|
327
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
328
|
+
box-shadow: 0 12px 30px rgba(0,0,0,0.45);
|
|
327
329
|
}
|
|
328
330
|
.context-menu-wrapper {
|
|
329
331
|
background: rgba(0,0,0,0.06) !important;
|
|
@@ -854,7 +856,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
854
856
|
<div class='tab-content'>
|
|
855
857
|
<% items.forEach((item) => { %>
|
|
856
858
|
<% if (item.profile) { %>
|
|
857
|
-
<a href="<%=item.url%>" class="tab connected"
|
|
859
|
+
<a href="<%=item.url%>" class="tab connected">
|
|
858
860
|
<div class='tab'>
|
|
859
861
|
<% if (item.image) { %>
|
|
860
862
|
<img class="app-icon" src="<%=item.image%>"/>
|
|
@@ -885,7 +887,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
885
887
|
</div>
|
|
886
888
|
</a>
|
|
887
889
|
<% } else { %>
|
|
888
|
-
<a href="<%=item.url%>" class="tab"
|
|
890
|
+
<a href="<%=item.url%>" class="tab">
|
|
889
891
|
<% if (item.image) { %>
|
|
890
892
|
<img class="icon" src="<%=item.image%>"/>
|
|
891
893
|
<% } else if (item.icon) { %>
|
package/server/views/index.ejs
CHANGED
|
@@ -158,8 +158,10 @@ body.dark .browser-options-row {
|
|
|
158
158
|
}
|
|
159
159
|
*/
|
|
160
160
|
body.dark .context-menu-wrapper {
|
|
161
|
-
background: rgba(0,0,0,0.9);
|
|
162
|
-
color: white;
|
|
161
|
+
background: rgba(0,0,0,0.9) !important;
|
|
162
|
+
color: white !important;
|
|
163
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
164
|
+
box-shadow: 0 12px 30px rgba(0,0,0,0.45);
|
|
163
165
|
}
|
|
164
166
|
.context-menu-wrapper {
|
|
165
167
|
background: whitesmoke !important;
|
|
@@ -194,6 +196,7 @@ body.dark .context-menu-wrapper {
|
|
|
194
196
|
padding: 6px 10px !important;
|
|
195
197
|
text-align: left;
|
|
196
198
|
color: black;
|
|
199
|
+
margin: 0 !important;
|
|
197
200
|
}
|
|
198
201
|
body.dark .btn {
|
|
199
202
|
background: rgba(255,255,255,0.05) !important;
|
|
@@ -209,6 +212,7 @@ body.dark .btn {
|
|
|
209
212
|
}
|
|
210
213
|
body.dark .context-menu .btn {
|
|
211
214
|
color: white;
|
|
215
|
+
background: none !important;
|
|
212
216
|
}
|
|
213
217
|
body.dark .open-menu, body.dark .browse {
|
|
214
218
|
border: none !important;
|
package/server/views/index2.ejs
CHANGED
|
@@ -128,8 +128,10 @@ body.dark .browser-options-row {
|
|
|
128
128
|
}
|
|
129
129
|
*/
|
|
130
130
|
body.dark .context-menu-wrapper {
|
|
131
|
-
background: rgba(0,0,0,0.9);
|
|
132
|
-
color: white;
|
|
131
|
+
background: rgba(0,0,0,0.9) !important;
|
|
132
|
+
color: white !important;
|
|
133
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
134
|
+
box-shadow: 0 12px 30px rgba(0,0,0,0.45);
|
|
133
135
|
}
|
|
134
136
|
.context-menu-wrapper {
|
|
135
137
|
background: rgba(0,0,0,0.06) !important;
|
package/server/views/net.ejs
CHANGED
|
@@ -135,8 +135,10 @@ body.dark .browser-options-row {
|
|
|
135
135
|
}
|
|
136
136
|
*/
|
|
137
137
|
body.dark .context-menu-wrapper {
|
|
138
|
-
background: rgba(0,0,0,0.9);
|
|
139
|
-
color: white;
|
|
138
|
+
background: rgba(0,0,0,0.9) !important;
|
|
139
|
+
color: white !important;
|
|
140
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
141
|
+
box-shadow: 0 12px 30px rgba(0,0,0,0.45);
|
|
140
142
|
}
|
|
141
143
|
.context-menu-wrapper {
|
|
142
144
|
background: rgba(0,0,0,0.06) !important;
|
|
@@ -782,9 +784,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
782
784
|
%>
|
|
783
785
|
<% if (directHosts.length > 0) { %>
|
|
784
786
|
<ul class="access-point-list">
|
|
785
|
-
<% directHosts.forEach((point) => {
|
|
787
|
+
<% directHosts.forEach((point) => {
|
|
788
|
+
const hasProtocol = /^https?:\/\//i.test(point.url)
|
|
789
|
+
const hrefValue = hasProtocol ? point.url : `http://${point.url}`
|
|
790
|
+
%>
|
|
786
791
|
<li>
|
|
787
|
-
<a class='net' target="_blank" href="<%=
|
|
792
|
+
<a class='net' target="_blank" href="<%= hrefValue %>"><%= point.url %></a>
|
|
788
793
|
<% if (point.badge) { %>
|
|
789
794
|
<span class="badge"><%= point.badge %></span>
|
|
790
795
|
<% } %>
|
package/server/views/network.ejs
CHANGED
|
@@ -651,8 +651,10 @@ body.dark .browser-options-row {
|
|
|
651
651
|
}
|
|
652
652
|
*/
|
|
653
653
|
body.dark .context-menu-wrapper {
|
|
654
|
-
background: rgba(0,0,0,0.9);
|
|
655
|
-
color: white;
|
|
654
|
+
background: rgba(0,0,0,0.9) !important;
|
|
655
|
+
color: white !important;
|
|
656
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
657
|
+
box-shadow: 0 12px 30px rgba(0,0,0,0.45);
|
|
656
658
|
}
|
|
657
659
|
.context-menu-wrapper {
|
|
658
660
|
background: rgba(0,0,0,0.06) !important;
|