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.
@@ -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
- // command line tools
25
+ const checkingMsg = "> checking xcode command line tools...\r\n"
26
+ console.log(checkingMsg)
27
+ ondata({ raw: checkingMsg })
23
28
 
24
- // try to get the contents
25
- let result = await this.kernel.bin.exec({ message: "ls -m $(xcode-select -p)" }, (stream) => {
26
- ondata(stream)
29
+ const cltStatus = await detectCommandLineTools({
30
+ exec: (params) => this.kernel.bin.exec(params, () => {})
27
31
  })
28
- let e5 = result && result.stdout && /.*Library.*/g.test(result.stdout) && /.*SDKs.*/g.test(result.stdout) && /.*usr.*/g.test(result.stdout)
29
- if (e5) {
30
- const msg = "> xcode-select command line tools is installed. checking the version...\r\n"
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
- // not installed. install
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" })
@@ -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
- let { stdout }= await this.exec({ message: "xcode-select -p", conda: { skip: true } }, (stream) => { })
465
- let e2 = /(.*Library.*Developer.*CommandLineTools.*|.*Xcode.*Developer.*)/gi.test(stdout)
466
- let e3 = await this.kernel.exists("/Library/Developer/CommandLineTools")
467
-
468
- // if xcode-select version exists
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
- this.socket.send(this.message, 0, this.message.length, this.port, '192.168.1.255');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.260.0",
3
+ "version": "3.262.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
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) {
@@ -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 { flex-direction: row; align-items: center; gap: 10px; }
56
- .tab-link-popover .tab-link-popover-item.qr-inline .textcol { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
57
- .tab-link-popover .tab-link-popover-item.qr-inline .qr { width: 64px; height: 64px; image-rendering: pixelated; flex: 0 0 auto; margin-left: auto; }
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 search = parsed.search || ""
276
- return `${host}${pathname}${search}`
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
- const popoverHeight = popover.offsetHeight
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 - 12) {
1248
- left = window.innerWidth - popoverWidth - 12
1257
+ if (left + popoverWidth > window.innerWidth - viewportPadding) {
1258
+ left = window.innerWidth - popoverWidth - viewportPadding
1249
1259
  }
1250
- if (left < 12) {
1251
- left = 12
1260
+ if (left < viewportPadding) {
1261
+ left = viewportPadding
1252
1262
  }
1253
1263
 
1254
- if (top + popoverHeight > window.innerHeight - 12) {
1255
- top = Math.max(12, rect.top - popoverHeight - 8)
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 && tabLinkPopoverEl.classList.contains("visible")) {
1613
- hideTabLinkPopover({ immediate: true })
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
 
@@ -7,9 +7,6 @@
7
7
  <style>
8
8
  body {
9
9
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
- max-width: 600px;
11
- margin: 50px auto;
12
- padding: 20px;
13
10
  background: #f8fafc;
14
11
  }
15
12
  .container {
@@ -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
- <a class='btn' href="https://pinokio.localhost/connect/<%=name%>">Get started<a>
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>
@@ -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" target="_blank">
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" target="_blank">
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) { %>
@@ -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;
@@ -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;
@@ -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="<%= point.url %>"><%= point.url %></a>
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
  <% } %>
@@ -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;