pinokiod 3.85.0 → 3.87.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/Dockerfile +61 -0
- package/docker-entrypoint.sh +75 -0
- package/kernel/api/hf/index.js +1 -1
- package/kernel/api/index.js +8 -1
- package/kernel/api/shell/index.js +6 -0
- package/kernel/api/terminal/index.js +166 -0
- package/kernel/bin/caddy.js +10 -4
- package/kernel/bin/conda.js +3 -2
- package/kernel/bin/index.js +53 -2
- package/kernel/bin/setup.js +32 -0
- package/kernel/bin/vs.js +11 -2
- package/kernel/index.js +42 -2
- package/kernel/info.js +36 -0
- package/kernel/peer.js +42 -18
- package/kernel/prototype.js +1 -0
- package/kernel/router/index.js +23 -15
- package/kernel/router/localhost_static_router.js +0 -3
- package/kernel/router/pinokio_domain_router.js +333 -0
- package/kernel/shell.js +43 -2
- package/kernel/shells.js +21 -1
- package/kernel/util.js +4 -2
- package/package.json +2 -1
- package/pipe/views/login.ejs +1 -1
- package/script/install-mode.js +33 -0
- package/script/pinokio.json +7 -0
- package/server/index.js +636 -246
- package/server/public/Socket.js +48 -0
- package/server/public/common.js +1956 -257
- package/server/public/fseditor.js +71 -12
- package/server/public/install.js +1 -1
- package/server/public/layout.js +740 -0
- package/server/public/modalinput.js +0 -1
- package/server/public/opener.js +12 -11
- package/server/public/serve/style.css +1 -1
- package/server/public/style.css +122 -129
- package/server/public/tab-idle-notifier.js +629 -0
- package/server/public/terminal_input_tracker.js +63 -0
- package/server/public/urldropdown.css +780 -45
- package/server/public/urldropdown.js +806 -156
- package/server/public/window_storage.js +97 -28
- package/server/socket.js +40 -9
- package/server/views/404.ejs +1 -1
- package/server/views/500.ejs +3 -3
- package/server/views/app.ejs +3146 -1381
- package/server/views/bookmarklet.ejs +197 -0
- package/server/views/bootstrap.ejs +1 -1
- package/server/views/columns.ejs +2 -13
- package/server/views/connect/x.ejs +4 -4
- package/server/views/connect.ejs +13 -14
- package/server/views/container.ejs +3 -4
- package/server/views/d.ejs +225 -55
- package/server/views/download.ejs +1 -1
- package/server/views/editor.ejs +2 -2
- package/server/views/env_editor.ejs +3 -3
- package/server/views/explore.ejs +2 -2
- package/server/views/file_explorer.ejs +3 -3
- package/server/views/git.ejs +7 -7
- package/server/views/github.ejs +3 -3
- package/server/views/help.ejs +2 -2
- package/server/views/index.ejs +17 -16
- package/server/views/index2.ejs +7 -7
- package/server/views/init/index.ejs +15 -79
- package/server/views/install.ejs +4 -4
- package/server/views/keys.ejs +2 -2
- package/server/views/layout.ejs +105 -0
- package/server/views/mini.ejs +2 -2
- package/server/views/net.ejs +45 -13
- package/server/views/network.ejs +41 -27
- package/server/views/network2.ejs +11 -11
- package/server/views/old_network.ejs +10 -10
- package/server/views/partials/dynamic.ejs +3 -5
- package/server/views/partials/menu.ejs +3 -5
- package/server/views/partials/running.ejs +1 -1
- package/server/views/pro.ejs +369 -0
- package/server/views/prototype/index.ejs +3 -3
- package/server/views/required_env_editor.ejs +2 -2
- package/server/views/review.ejs +15 -27
- package/server/views/rows.ejs +2 -13
- package/server/views/screenshots.ejs +298 -142
- package/server/views/settings.ejs +6 -7
- package/server/views/setup.ejs +3 -4
- package/server/views/setup_home.ejs +2 -2
- package/server/views/share_editor.ejs +4 -4
- package/server/views/shell.ejs +280 -29
- package/server/views/start.ejs +2 -2
- package/server/views/task.ejs +2 -2
- package/server/views/terminal.ejs +326 -52
- package/server/views/tools.ejs +461 -17
|
@@ -4,13 +4,64 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
function initUrlDropdown(config = {}) {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
if (window.PinokioUrlDropdown && typeof window.PinokioUrlDropdown.destroy === 'function') {
|
|
8
|
+
try {
|
|
9
|
+
window.PinokioUrlDropdown.destroy();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Failed to dispose existing URL dropdown', error);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let urlInput = document.querySelector('.urlbar input[type="url"]');
|
|
16
|
+
let dropdown = document.getElementById('url-dropdown');
|
|
9
17
|
const mobileButton = document.getElementById('mobile-link-button');
|
|
10
|
-
|
|
18
|
+
const mobileButtonHandler = () => showMobileModal();
|
|
19
|
+
|
|
20
|
+
const fallbackElements = {
|
|
21
|
+
form: null,
|
|
22
|
+
dropdown: null
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ensureFallbackInput = () => {
|
|
26
|
+
if (fallbackElements.form) {
|
|
27
|
+
const existingInput = fallbackElements.form.querySelector('input[type="url"]');
|
|
28
|
+
if (existingInput) return existingInput;
|
|
29
|
+
}
|
|
30
|
+
if (!document.body) return null;
|
|
31
|
+
const form = document.createElement('form');
|
|
32
|
+
form.className = 'urlbar pinokio-url-fallback';
|
|
33
|
+
form.id = 'pinokio-url-fallback-form';
|
|
34
|
+
form.style.display = 'none';
|
|
35
|
+
const input = document.createElement('input');
|
|
36
|
+
input.type = 'url';
|
|
37
|
+
form.appendChild(input);
|
|
38
|
+
document.body.appendChild(form);
|
|
39
|
+
fallbackElements.form = form;
|
|
40
|
+
return input;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const ensureFallbackDropdown = () => {
|
|
44
|
+
if (fallbackElements.dropdown) return fallbackElements.dropdown;
|
|
45
|
+
if (!document.body) return null;
|
|
46
|
+
const el = document.createElement('div');
|
|
47
|
+
el.id = 'url-dropdown';
|
|
48
|
+
el.className = 'url-dropdown';
|
|
49
|
+
el.style.display = 'none';
|
|
50
|
+
document.body.appendChild(el);
|
|
51
|
+
fallbackElements.dropdown = el;
|
|
52
|
+
return el;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (!urlInput) {
|
|
56
|
+
urlInput = ensureFallbackInput();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!dropdown) {
|
|
60
|
+
dropdown = ensureFallbackDropdown();
|
|
61
|
+
}
|
|
62
|
+
|
|
11
63
|
if (!urlInput || !dropdown) {
|
|
12
|
-
console.warn('URL dropdown elements not found');
|
|
13
|
-
return;
|
|
64
|
+
console.warn('URL dropdown elements not found; process picker modal will be limited.');
|
|
14
65
|
}
|
|
15
66
|
|
|
16
67
|
// Configuration options
|
|
@@ -21,9 +72,63 @@ function initUrlDropdown(config = {}) {
|
|
|
21
72
|
...config
|
|
22
73
|
};
|
|
23
74
|
|
|
75
|
+
const toArray = (value) => {
|
|
76
|
+
if (!value) return [];
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
return value.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
return [value];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getProcessUrls = (process) => {
|
|
84
|
+
const urls = (process && process.urls) || {};
|
|
85
|
+
const httpUrl = urls.http || (process && process.ip ? `http://${process.ip}` : null);
|
|
86
|
+
const httpsUrls = toArray(urls.https || (process && process.protocol === 'https' && process.url ? process.url : null))
|
|
87
|
+
.map((value) => {
|
|
88
|
+
if (typeof value !== 'string') return null;
|
|
89
|
+
const trimmed = value.trim();
|
|
90
|
+
if (!trimmed) return null;
|
|
91
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
92
|
+
return trimmed.replace(/^http:/i, 'https:');
|
|
93
|
+
}
|
|
94
|
+
return `https://${trimmed}`;
|
|
95
|
+
})
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
return { httpUrl, httpsUrls };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const getProcessDisplayUrl = (process) => {
|
|
101
|
+
if (process && typeof process.url === 'string' && process.url.trim().length > 0) {
|
|
102
|
+
return process.url;
|
|
103
|
+
}
|
|
104
|
+
const { httpUrl, httpsUrls } = getProcessUrls(process);
|
|
105
|
+
if (httpsUrls.length > 0) {
|
|
106
|
+
return httpsUrls[0];
|
|
107
|
+
}
|
|
108
|
+
return httpUrl;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const getProcessFilterValues = (process) => {
|
|
112
|
+
const urls = new Set();
|
|
113
|
+
const display = getProcessDisplayUrl(process);
|
|
114
|
+
if (display) {
|
|
115
|
+
urls.add(display);
|
|
116
|
+
}
|
|
117
|
+
const { httpUrl, httpsUrls } = getProcessUrls(process);
|
|
118
|
+
if (httpUrl) {
|
|
119
|
+
urls.add(httpUrl);
|
|
120
|
+
}
|
|
121
|
+
httpsUrls.forEach((httpsUrl) => urls.add(httpsUrl));
|
|
122
|
+
return Array.from(urls);
|
|
123
|
+
};
|
|
124
|
+
|
|
24
125
|
let isDropdownVisible = false;
|
|
25
126
|
let allProcesses = []; // Store all processes for filtering
|
|
26
127
|
let filteredProcesses = []; // Store currently filtered processes
|
|
128
|
+
let createLauncherModal = null;
|
|
129
|
+
let pendingCreateDetail = null;
|
|
130
|
+
let mobileModalKeydownHandler = null;
|
|
131
|
+
const EMPTY_STATE_DESCRIPTION = 'enter a prompt to create a launcher';
|
|
27
132
|
|
|
28
133
|
// Initialize input field state based on clear behavior
|
|
29
134
|
initializeInputValue();
|
|
@@ -36,17 +141,19 @@ function initUrlDropdown(config = {}) {
|
|
|
36
141
|
});
|
|
37
142
|
|
|
38
143
|
// Event listeners
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
144
|
+
if (urlInput) {
|
|
145
|
+
urlInput.addEventListener('focus', function() {
|
|
146
|
+
// Auto-select text for restore behavior to make filtering easier
|
|
147
|
+
if (options.clearBehavior === 'restore' && urlInput.value) {
|
|
148
|
+
// Use setTimeout to ensure the focus event completes first
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
urlInput.select();
|
|
151
|
+
}, 0);
|
|
152
|
+
}
|
|
153
|
+
showDropdown();
|
|
154
|
+
});
|
|
155
|
+
urlInput.addEventListener('input', handleInputChange);
|
|
156
|
+
}
|
|
50
157
|
|
|
51
158
|
// Hide dropdown when clicking outside
|
|
52
159
|
document.addEventListener('click', function(e) {
|
|
@@ -81,6 +188,7 @@ function initUrlDropdown(config = {}) {
|
|
|
81
188
|
|
|
82
189
|
|
|
83
190
|
function initializeInputValue() {
|
|
191
|
+
if (!urlInput) return;
|
|
84
192
|
if (options.clearBehavior === 'empty') {
|
|
85
193
|
urlInput.value = '';
|
|
86
194
|
} else if (options.clearBehavior === 'restore') {
|
|
@@ -92,6 +200,7 @@ function initUrlDropdown(config = {}) {
|
|
|
92
200
|
}
|
|
93
201
|
|
|
94
202
|
function showDropdown() {
|
|
203
|
+
if (!dropdown || !urlInput) return;
|
|
95
204
|
if (isDropdownVisible && allProcesses.length > 0) {
|
|
96
205
|
// If dropdown is already visible and we have data, show all initially
|
|
97
206
|
showAllProcesses();
|
|
@@ -135,6 +244,7 @@ function initUrlDropdown(config = {}) {
|
|
|
135
244
|
}
|
|
136
245
|
|
|
137
246
|
function handleInputChange() {
|
|
247
|
+
if (!urlInput) return;
|
|
138
248
|
if (!isDropdownVisible) return;
|
|
139
249
|
|
|
140
250
|
const query = urlInput.value.toLowerCase().trim();
|
|
@@ -149,11 +259,12 @@ function initUrlDropdown(config = {}) {
|
|
|
149
259
|
} else {
|
|
150
260
|
// Filter processes based on name and URL
|
|
151
261
|
filteredProcesses = allProcesses.filter(process => {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
262
|
+
const name = (process.name || '').toLowerCase();
|
|
263
|
+
if (name.includes(query)) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
const urls = getProcessFilterValues(process);
|
|
267
|
+
return urls.some((value) => (value || '').toLowerCase().includes(query));
|
|
157
268
|
});
|
|
158
269
|
}
|
|
159
270
|
|
|
@@ -162,7 +273,9 @@ function initUrlDropdown(config = {}) {
|
|
|
162
273
|
|
|
163
274
|
function hideDropdown() {
|
|
164
275
|
isDropdownVisible = false;
|
|
165
|
-
dropdown
|
|
276
|
+
if (dropdown) {
|
|
277
|
+
dropdown.style.display = 'none';
|
|
278
|
+
}
|
|
166
279
|
}
|
|
167
280
|
|
|
168
281
|
function createHostBadge(host) {
|
|
@@ -255,8 +368,6 @@ function initUrlDropdown(config = {}) {
|
|
|
255
368
|
platformIcon = 'fa-solid fa-desktop';
|
|
256
369
|
break;
|
|
257
370
|
}
|
|
258
|
-
|
|
259
|
-
console.log({ isLocal, host })
|
|
260
371
|
const hostName = isLocal ? `${host.name} (This Machine)` : `${host.name} (Peer)`;
|
|
261
372
|
|
|
262
373
|
return `
|
|
@@ -271,19 +382,63 @@ function initUrlDropdown(config = {}) {
|
|
|
271
382
|
}
|
|
272
383
|
|
|
273
384
|
function populateDropdown(processes) {
|
|
385
|
+
const currentUrl = window.location.href;
|
|
386
|
+
const currentTitle = document.title || 'Current tab';
|
|
387
|
+
|
|
388
|
+
let html = '';
|
|
389
|
+
if (currentUrl) {
|
|
390
|
+
html += `
|
|
391
|
+
<div class="url-dropdown-host-header current-tab">
|
|
392
|
+
<span class="host-name">Current tab</span>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="url-dropdown-item" data-url="${escapeHtml(currentUrl)}" data-host-type="current">
|
|
395
|
+
<div class="url-dropdown-name">
|
|
396
|
+
<span>
|
|
397
|
+
<i class="fa-solid fa-clone"></i>
|
|
398
|
+
${escapeHtml(currentTitle)}
|
|
399
|
+
</span>
|
|
400
|
+
</div>
|
|
401
|
+
<div class="url-dropdown-url">${escapeHtml(currentUrl)}</div>
|
|
402
|
+
</div>
|
|
403
|
+
`;
|
|
404
|
+
}
|
|
405
|
+
|
|
274
406
|
if (processes.length === 0) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
407
|
+
html += createEmptyStateHtml(getEmptyStateMessage(urlInput));
|
|
408
|
+
dropdown.innerHTML = html;
|
|
409
|
+
attachCreateButtonHandler(dropdown, urlInput);
|
|
410
|
+
dropdown.querySelectorAll('.url-dropdown-item:not(.non-selectable)').forEach(item => {
|
|
411
|
+
item.addEventListener('click', function() {
|
|
412
|
+
const url = this.getAttribute('data-url');
|
|
413
|
+
const type = this.getAttribute('data-host-type');
|
|
414
|
+
urlInput.value = url;
|
|
415
|
+
urlInput.setAttribute("data-host-type", type || 'current');
|
|
416
|
+
hideDropdown();
|
|
417
|
+
|
|
418
|
+
if (type === "local") {
|
|
419
|
+
let redirect_uri = "/container?url=" + url;
|
|
420
|
+
location.href = redirect_uri;
|
|
421
|
+
} else {
|
|
422
|
+
if (!type || type === 'current') {
|
|
423
|
+
location.href = url;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
let u = new URL(url);
|
|
427
|
+
if (String(u.port) === "42000") {
|
|
428
|
+
window.open(url, "_blank", 'self');
|
|
429
|
+
} else {
|
|
430
|
+
let redirect_uri = "/container?url=" + url;
|
|
431
|
+
location.href = redirect_uri;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|
|
280
436
|
return;
|
|
281
437
|
}
|
|
282
438
|
|
|
283
439
|
// Group processes by host
|
|
284
440
|
const groupedProcesses = groupProcessesByHost(processes);
|
|
285
|
-
|
|
286
|
-
let html = '';
|
|
441
|
+
|
|
287
442
|
Object.keys(groupedProcesses).forEach(hostKey => {
|
|
288
443
|
const hostData = groupedProcesses[hostKey];
|
|
289
444
|
const hostInfo = hostData.host;
|
|
@@ -313,7 +468,10 @@ function initUrlDropdown(config = {}) {
|
|
|
313
468
|
`;
|
|
314
469
|
} else {
|
|
315
470
|
// Normal selectable item
|
|
316
|
-
const url =
|
|
471
|
+
const url = getProcessDisplayUrl(process);
|
|
472
|
+
if (!url) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
317
475
|
html += `
|
|
318
476
|
<div class="url-dropdown-item" data-url="${url}" data-host-type="${process.host.local ? "local" : "remote"}">
|
|
319
477
|
<div class="url-dropdown-name">
|
|
@@ -335,13 +493,15 @@ function initUrlDropdown(config = {}) {
|
|
|
335
493
|
const url = this.getAttribute('data-url');
|
|
336
494
|
const type = this.getAttribute('data-host-type');
|
|
337
495
|
urlInput.value = url;
|
|
338
|
-
urlInput.setAttribute("data-host-type", type);
|
|
496
|
+
urlInput.setAttribute("data-host-type", type || 'remote');
|
|
339
497
|
hideDropdown();
|
|
340
498
|
|
|
341
499
|
// Navigate directly instead of dispatching submit event
|
|
342
500
|
if (type === "local") {
|
|
343
501
|
let redirect_uri = "/container?url=" + url;
|
|
344
502
|
location.href = redirect_uri;
|
|
503
|
+
} else if (type === 'current') {
|
|
504
|
+
location.href = url;
|
|
345
505
|
} else {
|
|
346
506
|
let u = new URL(url);
|
|
347
507
|
if (String(u.port) === "42000") {
|
|
@@ -371,98 +531,345 @@ function initUrlDropdown(config = {}) {
|
|
|
371
531
|
return div.innerHTML;
|
|
372
532
|
}
|
|
373
533
|
|
|
534
|
+
function getModalOverlay() {
|
|
535
|
+
return document.getElementById('url-modal-overlay');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function getModalRefs() {
|
|
539
|
+
const overlay = getModalOverlay();
|
|
540
|
+
return overlay ? overlay._modalRefs : null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function resolveModal(refs, value) {
|
|
544
|
+
if (!refs || typeof refs.resolve !== 'function') {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const resolver = refs.resolve;
|
|
548
|
+
refs.resolve = null;
|
|
549
|
+
refs.returnSelection = false;
|
|
550
|
+
try {
|
|
551
|
+
resolver(value);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
console.error('Failed to resolve URL modal selection', err);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function buildPaneUrl(url, type) {
|
|
558
|
+
if (!url || typeof url !== 'string') return url;
|
|
559
|
+
|
|
560
|
+
const ensureContainer = () => {
|
|
561
|
+
if (url.startsWith('/container?url=')) return url;
|
|
562
|
+
return `/container?url=${encodeURIComponent(url)}`;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
switch (type) {
|
|
566
|
+
case 'current':
|
|
567
|
+
return url;
|
|
568
|
+
case 'local':
|
|
569
|
+
return ensureContainer();
|
|
570
|
+
case 'remote':
|
|
571
|
+
try {
|
|
572
|
+
const parsed = new URL(url);
|
|
573
|
+
if (String(parsed.port) === '42000') {
|
|
574
|
+
return url;
|
|
575
|
+
}
|
|
576
|
+
} catch (_) {
|
|
577
|
+
// If URL constructor fails, fall back to container redirect
|
|
578
|
+
}
|
|
579
|
+
return ensureContainer();
|
|
580
|
+
default:
|
|
581
|
+
return ensureContainer();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function handleModalSelection(url, type) {
|
|
586
|
+
const refs = getModalRefs();
|
|
587
|
+
if (!refs) return;
|
|
588
|
+
|
|
589
|
+
const paneUrl = buildPaneUrl(url, type);
|
|
590
|
+
|
|
591
|
+
if (refs.input) {
|
|
592
|
+
refs.input.value = paneUrl;
|
|
593
|
+
if (typeof refs.updateConfirmState === 'function') {
|
|
594
|
+
refs.updateConfirmState();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (!refs.returnSelection && urlInput) {
|
|
599
|
+
urlInput.value = paneUrl;
|
|
600
|
+
urlInput.setAttribute('data-host-type', type || 'remote');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (refs.returnSelection) {
|
|
604
|
+
resolveModal(refs, paneUrl);
|
|
605
|
+
closeMobileModal({ suppressResolve: true });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
closeMobileModal();
|
|
610
|
+
|
|
611
|
+
if (!type || type === 'current') {
|
|
612
|
+
location.href = paneUrl;
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (type === 'local' || type === 'remote') {
|
|
617
|
+
if (paneUrl.startsWith('/container?url=')) {
|
|
618
|
+
location.href = paneUrl;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const parsed = new URL(paneUrl);
|
|
623
|
+
if (String(parsed.port) === '42000') {
|
|
624
|
+
window.open(paneUrl, '_blank', 'self');
|
|
625
|
+
} else {
|
|
626
|
+
location.href = `/container?url=${encodeURIComponent(paneUrl)}`;
|
|
627
|
+
}
|
|
628
|
+
} catch (error) {
|
|
629
|
+
console.error('Failed to open URL, redirecting directly', error);
|
|
630
|
+
location.href = paneUrl;
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
location.href = paneUrl;
|
|
636
|
+
}
|
|
637
|
+
|
|
374
638
|
// Mobile modal functionality
|
|
375
639
|
function createMobileModal() {
|
|
376
640
|
const overlay = document.createElement('div');
|
|
377
|
-
overlay.className = 'url-modal-overlay';
|
|
641
|
+
overlay.className = 'modal-overlay url-modal-overlay';
|
|
378
642
|
overlay.id = 'url-modal-overlay';
|
|
379
|
-
|
|
643
|
+
|
|
380
644
|
const content = document.createElement('div');
|
|
381
645
|
content.className = 'url-modal-content';
|
|
382
|
-
|
|
383
|
-
|
|
646
|
+
content.setAttribute('role', 'dialog');
|
|
647
|
+
content.setAttribute('aria-modal', 'true');
|
|
648
|
+
|
|
649
|
+
const closeButton = document.createElement('button');
|
|
650
|
+
closeButton.type = 'button';
|
|
384
651
|
closeButton.className = 'url-modal-close';
|
|
652
|
+
closeButton.setAttribute('aria-label', 'Close');
|
|
385
653
|
closeButton.innerHTML = '×';
|
|
386
|
-
|
|
387
|
-
|
|
654
|
+
|
|
655
|
+
const heading = document.createElement('h3');
|
|
656
|
+
heading.textContent = 'Open a URL';
|
|
657
|
+
heading.id = 'url-modal-title';
|
|
658
|
+
|
|
659
|
+
const description = document.createElement('p');
|
|
660
|
+
description.className = 'url-modal-description';
|
|
661
|
+
description.id = 'url-modal-description';
|
|
662
|
+
description.textContent = 'Enter a local URL or choose from running processes.';
|
|
663
|
+
|
|
664
|
+
content.setAttribute('aria-labelledby', heading.id);
|
|
665
|
+
content.setAttribute('aria-describedby', description.id);
|
|
666
|
+
|
|
388
667
|
const modalInput = document.createElement('input');
|
|
389
668
|
modalInput.type = 'url';
|
|
390
669
|
modalInput.className = 'url-modal-input';
|
|
391
|
-
modalInput.placeholder = '
|
|
392
|
-
|
|
670
|
+
modalInput.placeholder = 'Example: http://localhost:7860';
|
|
671
|
+
|
|
393
672
|
const modalDropdown = document.createElement('div');
|
|
394
673
|
modalDropdown.className = 'url-dropdown';
|
|
395
674
|
modalDropdown.id = 'url-modal-dropdown';
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
675
|
+
|
|
676
|
+
const actions = document.createElement('div');
|
|
677
|
+
actions.className = 'url-modal-actions';
|
|
678
|
+
|
|
679
|
+
const cancelButton = document.createElement('button');
|
|
680
|
+
cancelButton.type = 'button';
|
|
681
|
+
cancelButton.className = 'url-modal-button cancel';
|
|
682
|
+
cancelButton.textContent = 'Cancel';
|
|
683
|
+
|
|
684
|
+
const confirmButton = document.createElement('button');
|
|
685
|
+
confirmButton.type = 'button';
|
|
686
|
+
confirmButton.className = 'url-modal-button confirm';
|
|
687
|
+
confirmButton.textContent = 'Open';
|
|
688
|
+
confirmButton.disabled = true;
|
|
689
|
+
|
|
690
|
+
actions.append(cancelButton, confirmButton);
|
|
691
|
+
|
|
692
|
+
content.append(closeButton, heading, description, modalInput, modalDropdown, actions);
|
|
693
|
+
overlay.append(content);
|
|
694
|
+
|
|
695
|
+
const updateConfirmState = () => {
|
|
696
|
+
confirmButton.disabled = !modalInput.value.trim();
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
modalInput.addEventListener('focus', () => {
|
|
700
|
+
if (options.clearBehavior === 'restore' && modalInput.value) {
|
|
701
|
+
setTimeout(() => modalInput.select(), 0);
|
|
702
|
+
}
|
|
703
|
+
updateConfirmState();
|
|
704
|
+
showModalDropdown(modalDropdown);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
modalInput.addEventListener('input', () => {
|
|
708
|
+
handleModalInputChange(modalInput, modalDropdown);
|
|
709
|
+
updateConfirmState();
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
modalInput.addEventListener('keydown', (event) => {
|
|
713
|
+
if (event.key === 'Enter') {
|
|
714
|
+
event.preventDefault();
|
|
715
|
+
submitMobileModal();
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
cancelButton.addEventListener('click', closeMobileModal);
|
|
720
|
+
confirmButton.addEventListener('click', submitMobileModal);
|
|
721
|
+
closeButton.addEventListener('click', closeMobileModal);
|
|
722
|
+
|
|
723
|
+
overlay.addEventListener('click', (event) => {
|
|
724
|
+
if (event.target === overlay) {
|
|
725
|
+
closeMobileModal();
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
overlay._modalRefs = {
|
|
730
|
+
input: modalInput,
|
|
731
|
+
dropdown: modalDropdown,
|
|
732
|
+
confirmButton,
|
|
733
|
+
cancelButton,
|
|
734
|
+
closeButton,
|
|
735
|
+
heading,
|
|
736
|
+
description,
|
|
737
|
+
updateConfirmState,
|
|
738
|
+
defaults: {
|
|
739
|
+
title: heading.textContent,
|
|
740
|
+
description: description.textContent,
|
|
741
|
+
confirmLabel: confirmButton.textContent
|
|
742
|
+
},
|
|
743
|
+
context: 'default',
|
|
744
|
+
returnSelection: false,
|
|
745
|
+
includeCurrent: true,
|
|
746
|
+
resolve: null
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
return overlay;
|
|
408
750
|
}
|
|
409
|
-
|
|
410
|
-
function showMobileModal() {
|
|
411
|
-
let
|
|
412
|
-
if (!
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
751
|
+
|
|
752
|
+
function showMobileModal(customOptions = {}) {
|
|
753
|
+
let overlay = document.getElementById('url-modal-overlay');
|
|
754
|
+
if (!overlay) {
|
|
755
|
+
overlay = createMobileModal();
|
|
756
|
+
document.body.appendChild(overlay);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const refs = overlay._modalRefs || {};
|
|
760
|
+
const modalInput = refs.input;
|
|
761
|
+
const updateConfirmState = refs.updateConfirmState;
|
|
762
|
+
if (!modalInput || !updateConfirmState) return undefined;
|
|
763
|
+
|
|
764
|
+
const defaults = refs.defaults || {};
|
|
765
|
+
const title = customOptions.title || defaults.title || 'Open a URL';
|
|
766
|
+
const descriptionText = customOptions.description || defaults.description || 'Enter a local URL or choose from running processes.';
|
|
767
|
+
const confirmLabel = customOptions.confirmLabel || defaults.confirmLabel || 'Open';
|
|
768
|
+
const includeCurrent = customOptions.includeCurrent !== false;
|
|
769
|
+
const initialValue = customOptions.initialValue !== undefined
|
|
770
|
+
? customOptions.initialValue
|
|
771
|
+
: (options.clearBehavior === 'restore'
|
|
772
|
+
? ((urlInput && urlInput.value) || options.defaultValue || '')
|
|
773
|
+
: '');
|
|
774
|
+
|
|
775
|
+
refs.heading.textContent = title;
|
|
776
|
+
refs.description.textContent = descriptionText;
|
|
777
|
+
refs.confirmButton.textContent = confirmLabel;
|
|
778
|
+
refs.includeCurrent = includeCurrent;
|
|
779
|
+
refs.context = customOptions.context || 'default';
|
|
780
|
+
refs.returnSelection = Boolean(customOptions.awaitSelection);
|
|
781
|
+
refs.resolve = null;
|
|
782
|
+
|
|
783
|
+
modalInput.value = initialValue;
|
|
784
|
+
updateConfirmState();
|
|
785
|
+
|
|
786
|
+
requestAnimationFrame(() => {
|
|
787
|
+
overlay.classList.add('is-visible');
|
|
788
|
+
requestAnimationFrame(() => modalInput.focus());
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
if (!mobileModalKeydownHandler) {
|
|
792
|
+
mobileModalKeydownHandler = (event) => {
|
|
793
|
+
if (event.key === 'Escape') {
|
|
794
|
+
event.preventDefault();
|
|
431
795
|
closeMobileModal();
|
|
432
796
|
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
urlInput.closest('form').dispatchEvent(new Event('submit'));
|
|
442
|
-
closeMobileModal();
|
|
443
|
-
}
|
|
444
|
-
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
document.addEventListener('keydown', mobileModalKeydownHandler, true);
|
|
801
|
+
|
|
802
|
+
if (refs.returnSelection) {
|
|
803
|
+
return new Promise((resolve) => {
|
|
804
|
+
refs.resolve = resolve;
|
|
445
805
|
});
|
|
446
806
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
807
|
+
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function closeMobileModal(options = {}) {
|
|
812
|
+
const overlay = getModalOverlay();
|
|
813
|
+
if (!overlay) return;
|
|
814
|
+
overlay.classList.remove('is-visible');
|
|
815
|
+
const refs = overlay._modalRefs;
|
|
816
|
+
|
|
817
|
+
if (refs?.dropdown) {
|
|
818
|
+
refs.dropdown.style.display = 'none';
|
|
819
|
+
}
|
|
820
|
+
if (refs?.confirmButton) {
|
|
821
|
+
refs.confirmButton.disabled = true;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (refs) {
|
|
825
|
+
if (options.resolveValue !== undefined) {
|
|
826
|
+
resolveModal(refs, options.resolveValue);
|
|
827
|
+
} else if (refs.returnSelection && options.suppressResolve !== true) {
|
|
828
|
+
resolveModal(refs, null);
|
|
829
|
+
} else if (!refs.returnSelection || options.keepMode) {
|
|
830
|
+
// Preserve resolver when explicitly requested
|
|
831
|
+
} else {
|
|
832
|
+
refs.resolve = null;
|
|
833
|
+
refs.returnSelection = false;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (!options.keepMode) {
|
|
837
|
+
refs.context = 'default';
|
|
838
|
+
refs.includeCurrent = true;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (mobileModalKeydownHandler) {
|
|
843
|
+
document.removeEventListener('keydown', mobileModalKeydownHandler, true);
|
|
844
|
+
mobileModalKeydownHandler = null;
|
|
456
845
|
}
|
|
457
|
-
|
|
458
|
-
setTimeout(() => modalInput.focus(), 100);
|
|
459
846
|
}
|
|
460
|
-
|
|
461
|
-
function
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
464
|
-
|
|
847
|
+
|
|
848
|
+
function submitMobileModal() {
|
|
849
|
+
const refs = getModalRefs();
|
|
850
|
+
if (!refs || !refs.input) return;
|
|
851
|
+
const input = refs.input;
|
|
852
|
+
const value = input.value.trim();
|
|
853
|
+
if (!value) return;
|
|
854
|
+
|
|
855
|
+
if (refs.returnSelection) {
|
|
856
|
+
const paneUrl = buildPaneUrl(value, 'remote');
|
|
857
|
+
resolveModal(refs, paneUrl);
|
|
858
|
+
closeMobileModal({ suppressResolve: true });
|
|
859
|
+
return;
|
|
465
860
|
}
|
|
861
|
+
|
|
862
|
+
if (urlInput) {
|
|
863
|
+
const paneUrl = buildPaneUrl(value, 'remote');
|
|
864
|
+
urlInput.value = paneUrl;
|
|
865
|
+
const form = urlInput.closest('form');
|
|
866
|
+
if (form) {
|
|
867
|
+
form.dispatchEvent(new Event('submit'));
|
|
868
|
+
} else {
|
|
869
|
+
location.href = paneUrl;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
closeMobileModal();
|
|
466
873
|
}
|
|
467
874
|
|
|
468
875
|
function showModalDropdown(modalDropdown) {
|
|
@@ -500,10 +907,12 @@ function initUrlDropdown(config = {}) {
|
|
|
500
907
|
filtered = allProcesses;
|
|
501
908
|
} else if (query) {
|
|
502
909
|
filtered = allProcesses.filter(process => {
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
910
|
+
const name = (process.name || '').toLowerCase();
|
|
911
|
+
if (name.includes(query)) {
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
const urls = getProcessFilterValues(process);
|
|
915
|
+
return urls.some((value) => (value || '').toLowerCase().includes(query));
|
|
507
916
|
});
|
|
508
917
|
}
|
|
509
918
|
|
|
@@ -512,35 +921,58 @@ function initUrlDropdown(config = {}) {
|
|
|
512
921
|
|
|
513
922
|
function populateModalDropdown(processes, modalDropdown) {
|
|
514
923
|
const modalInput = modalDropdown.parentElement.querySelector('.url-modal-input');
|
|
515
|
-
|
|
924
|
+
const currentUrl = window.location.href;
|
|
925
|
+
const currentTitle = document.title || 'Current tab';
|
|
926
|
+
const overlayRefs = getModalRefs();
|
|
927
|
+
const includeCurrent = overlayRefs?.includeCurrent !== false;
|
|
928
|
+
|
|
929
|
+
let html = '';
|
|
930
|
+
|
|
931
|
+
if (includeCurrent && currentUrl) {
|
|
932
|
+
html += `
|
|
933
|
+
<div class="url-dropdown-host-header current-tab">
|
|
934
|
+
<span class="host-name">Current tab</span>
|
|
935
|
+
</div>
|
|
936
|
+
<div class="url-dropdown-item" data-url="${escapeHtml(currentUrl)}" data-host-type="current">
|
|
937
|
+
<div class="url-dropdown-name">
|
|
938
|
+
<i class="fa-solid fa-clone"></i>
|
|
939
|
+
<span>${escapeHtml(currentTitle)}</span>
|
|
940
|
+
</div>
|
|
941
|
+
<div class="url-dropdown-url">${escapeHtml(currentUrl)}</div>
|
|
942
|
+
</div>
|
|
943
|
+
`;
|
|
944
|
+
}
|
|
945
|
+
|
|
516
946
|
if (processes.length === 0) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
modalDropdown
|
|
947
|
+
html += createEmptyStateHtml(getEmptyStateMessage(modalInput));
|
|
948
|
+
modalDropdown.innerHTML = html;
|
|
949
|
+
attachCreateButtonHandler(modalDropdown, modalInput);
|
|
950
|
+
|
|
951
|
+
modalDropdown.querySelectorAll('.url-dropdown-item:not(.non-selectable)').forEach(item => {
|
|
952
|
+
item.addEventListener('click', function() {
|
|
953
|
+
const url = this.getAttribute('data-url');
|
|
954
|
+
const type = this.getAttribute('data-host-type');
|
|
955
|
+
handleModalSelection(url, type);
|
|
956
|
+
});
|
|
957
|
+
});
|
|
520
958
|
return;
|
|
521
959
|
}
|
|
522
960
|
|
|
523
|
-
// Group processes by host
|
|
524
961
|
const groupedProcesses = groupProcessesByHost(processes);
|
|
525
|
-
|
|
526
|
-
let html = '';
|
|
527
962
|
Object.keys(groupedProcesses).forEach(hostKey => {
|
|
528
963
|
const hostData = groupedProcesses[hostKey];
|
|
529
964
|
const hostInfo = hostData.host;
|
|
530
|
-
const
|
|
965
|
+
const hostProcesses = hostData.processes;
|
|
531
966
|
const isLocal = hostData.isLocal;
|
|
532
|
-
|
|
533
|
-
// Add host header
|
|
967
|
+
|
|
534
968
|
html += createHostHeader(hostInfo, isLocal);
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
'<div class="status-circle online"></div>' :
|
|
969
|
+
|
|
970
|
+
hostProcesses.forEach(process => {
|
|
971
|
+
const onlineIndicator = process.online ?
|
|
972
|
+
'<div class="status-circle online"></div>' :
|
|
540
973
|
'<div class="status-circle offline"></div>';
|
|
541
|
-
|
|
974
|
+
|
|
542
975
|
if (process.ip === null || process.ip === undefined) {
|
|
543
|
-
// Non-selectable item with "turn on peer network" button
|
|
544
976
|
const networkUrl = `http://${process.host.ip}:42000/network`;
|
|
545
977
|
html += `
|
|
546
978
|
<div class="url-dropdown-item non-selectable">
|
|
@@ -552,12 +984,16 @@ function initUrlDropdown(config = {}) {
|
|
|
552
984
|
</div>
|
|
553
985
|
`;
|
|
554
986
|
} else {
|
|
555
|
-
|
|
556
|
-
|
|
987
|
+
const url = getProcessDisplayUrl(process);
|
|
988
|
+
if (!url) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
557
991
|
html += `
|
|
558
992
|
<div class="url-dropdown-item" data-url="${url}" data-host-type="${process.host.local ? "local" : "remote"}">
|
|
559
|
-
|
|
560
|
-
|
|
993
|
+
<div class="url-dropdown-name">
|
|
994
|
+
${onlineIndicator}
|
|
995
|
+
${escapeHtml(process.name)}
|
|
996
|
+
</div>
|
|
561
997
|
<div class="url-dropdown-url">${escapeHtml(url)}</div>
|
|
562
998
|
</div>
|
|
563
999
|
`;
|
|
@@ -571,28 +1007,10 @@ function initUrlDropdown(config = {}) {
|
|
|
571
1007
|
item.addEventListener('click', function() {
|
|
572
1008
|
const url = this.getAttribute('data-url');
|
|
573
1009
|
const type = this.getAttribute('data-host-type');
|
|
574
|
-
|
|
575
|
-
urlInput.value = url;
|
|
576
|
-
urlInput.setAttribute("data-host-type", type);
|
|
577
|
-
closeMobileModal();
|
|
578
|
-
|
|
579
|
-
// Navigate directly instead of dispatching submit event
|
|
580
|
-
if (type === "local") {
|
|
581
|
-
let redirect_uri = "/container?url=" + url;
|
|
582
|
-
location.href = redirect_uri;
|
|
583
|
-
} else {
|
|
584
|
-
let u = new URL(url);
|
|
585
|
-
if (String(u.port) === "42000") {
|
|
586
|
-
window.open(url, "_blank", 'self');
|
|
587
|
-
} else {
|
|
588
|
-
let redirect_uri = "/container?url=" + url;
|
|
589
|
-
location.href = redirect_uri;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
1010
|
+
handleModalSelection(url, type);
|
|
592
1011
|
});
|
|
593
1012
|
});
|
|
594
1013
|
|
|
595
|
-
// Add click handlers to peer network buttons in modal
|
|
596
1014
|
modalDropdown.querySelectorAll('.peer-network-button').forEach(button => {
|
|
597
1015
|
button.addEventListener('click', function(e) {
|
|
598
1016
|
e.stopPropagation();
|
|
@@ -604,16 +1022,15 @@ function initUrlDropdown(config = {}) {
|
|
|
604
1022
|
|
|
605
1023
|
// Set up mobile button click handler
|
|
606
1024
|
if (mobileButton) {
|
|
607
|
-
mobileButton.addEventListener('click',
|
|
1025
|
+
mobileButton.addEventListener('click', mobileButtonHandler);
|
|
608
1026
|
}
|
|
609
1027
|
|
|
610
|
-
|
|
611
|
-
return {
|
|
1028
|
+
const api = {
|
|
612
1029
|
show: showDropdown,
|
|
613
1030
|
hide: hideDropdown,
|
|
614
1031
|
showAll: showAllProcesses,
|
|
615
|
-
showMobileModal
|
|
616
|
-
closeMobileModal
|
|
1032
|
+
showMobileModal,
|
|
1033
|
+
closeMobileModal,
|
|
617
1034
|
refresh: function() {
|
|
618
1035
|
allProcesses = []; // Clear cache to force refetch
|
|
619
1036
|
if (isDropdownVisible) {
|
|
@@ -621,18 +1038,251 @@ function initUrlDropdown(config = {}) {
|
|
|
621
1038
|
}
|
|
622
1039
|
},
|
|
623
1040
|
filter: handleInputChange,
|
|
1041
|
+
openSplitModal: function(modalOptions = {}) {
|
|
1042
|
+
return showMobileModal({
|
|
1043
|
+
title: modalOptions.title || 'Split View',
|
|
1044
|
+
description: modalOptions.description || 'Choose a running process or use the current tab URL for the new pane.',
|
|
1045
|
+
confirmLabel: modalOptions.confirmLabel || 'Split',
|
|
1046
|
+
includeCurrent: modalOptions.includeCurrent !== false,
|
|
1047
|
+
awaitSelection: true,
|
|
1048
|
+
context: 'split'
|
|
1049
|
+
});
|
|
1050
|
+
},
|
|
624
1051
|
destroy: function() {
|
|
625
|
-
|
|
626
|
-
|
|
1052
|
+
if (urlInput) {
|
|
1053
|
+
urlInput.removeEventListener('input', handleInputChange);
|
|
1054
|
+
}
|
|
627
1055
|
if (mobileButton) {
|
|
628
|
-
mobileButton.removeEventListener('click',
|
|
1056
|
+
mobileButton.removeEventListener('click', mobileButtonHandler);
|
|
629
1057
|
}
|
|
630
1058
|
hideDropdown();
|
|
631
|
-
closeMobileModal();
|
|
1059
|
+
closeMobileModal({ suppressResolve: true });
|
|
632
1060
|
allProcesses = [];
|
|
633
1061
|
filteredProcesses = [];
|
|
1062
|
+
if (fallbackElements.form && fallbackElements.form.parentElement) {
|
|
1063
|
+
fallbackElements.form.parentElement.removeChild(fallbackElements.form);
|
|
1064
|
+
}
|
|
1065
|
+
if (fallbackElements.dropdown && fallbackElements.dropdown.parentElement) {
|
|
1066
|
+
fallbackElements.dropdown.parentElement.removeChild(fallbackElements.dropdown);
|
|
1067
|
+
}
|
|
1068
|
+
fallbackElements.form = null;
|
|
1069
|
+
fallbackElements.dropdown = null;
|
|
1070
|
+
if (window.PinokioUrlDropdown === api) {
|
|
1071
|
+
window.PinokioUrlDropdown = null;
|
|
1072
|
+
}
|
|
634
1073
|
}
|
|
635
1074
|
};
|
|
1075
|
+
|
|
1076
|
+
window.PinokioUrlDropdown = api;
|
|
1077
|
+
return api;
|
|
1078
|
+
function showEmptyState(container, inputElement) {
|
|
1079
|
+
container.innerHTML = createEmptyStateHtml(getEmptyStateMessage(inputElement));
|
|
1080
|
+
attachCreateButtonHandler(container, inputElement);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function getEmptyStateMessage(inputElement) {
|
|
1084
|
+
const rawValue = inputElement.value.trim();
|
|
1085
|
+
return rawValue ? `No processes match "${rawValue}"` : 'No running processes found';
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function createEmptyStateHtml(message) {
|
|
1089
|
+
return `
|
|
1090
|
+
<div class="url-dropdown-empty">
|
|
1091
|
+
<div class="url-dropdown-empty-message">${escapeHtml(message)}</div>
|
|
1092
|
+
<div class="url-dropdown-empty-actions">
|
|
1093
|
+
<button type="button" class="url-dropdown-create-button">Create</button>
|
|
1094
|
+
<div class="url-dropdown-empty-description">${escapeHtml(EMPTY_STATE_DESCRIPTION)}</div>
|
|
1095
|
+
</div>
|
|
1096
|
+
</div>
|
|
1097
|
+
`;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function attachCreateButtonHandler(container, inputElement) {
|
|
1101
|
+
const createButton = container.querySelector('.url-dropdown-create-button');
|
|
1102
|
+
if (!createButton) return;
|
|
1103
|
+
|
|
1104
|
+
createButton.addEventListener('click', function(event) {
|
|
1105
|
+
event.preventDefault();
|
|
1106
|
+
event.stopPropagation();
|
|
1107
|
+
const prompt = inputElement.value.trim();
|
|
1108
|
+
const detail = {
|
|
1109
|
+
query: prompt,
|
|
1110
|
+
prompt,
|
|
1111
|
+
input: inputElement,
|
|
1112
|
+
dropdown: container,
|
|
1113
|
+
context: inputElement === urlInput ? 'dropdown' : 'modal'
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
if (detail.context === 'dropdown') {
|
|
1117
|
+
hideDropdown();
|
|
1118
|
+
} else {
|
|
1119
|
+
closeMobileModal();
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
showCreateLauncherModal(detail);
|
|
1123
|
+
|
|
1124
|
+
if (typeof options.onCreate === 'function') {
|
|
1125
|
+
options.onCreate(detail);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (typeof CustomEvent === 'function') {
|
|
1129
|
+
container.dispatchEvent(new CustomEvent('urlDropdownCreate', { detail }));
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function showCreateLauncherModal(detail) {
|
|
1135
|
+
const modal = getCreateLauncherModal();
|
|
1136
|
+
pendingCreateDetail = detail;
|
|
1137
|
+
|
|
1138
|
+
modal.error.textContent = '';
|
|
1139
|
+
// const defaultName = generateFolderName(detail.prompt);
|
|
1140
|
+
modal.input.value = '';
|
|
1141
|
+
modal.description.textContent = detail.prompt
|
|
1142
|
+
? `Prompt: ${detail.prompt}`
|
|
1143
|
+
: 'Enter a prompt in the search bar to describe your launcher.';
|
|
1144
|
+
|
|
1145
|
+
requestAnimationFrame(() => {
|
|
1146
|
+
modal.overlay.classList.add('is-visible');
|
|
1147
|
+
requestAnimationFrame(() => {
|
|
1148
|
+
modal.input.focus();
|
|
1149
|
+
modal.input.select();
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
document.addEventListener('keydown', handleCreateModalEscape, true);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function hideCreateLauncherModal() {
|
|
1157
|
+
const modal = createLauncherModal;
|
|
1158
|
+
if (!modal) return;
|
|
1159
|
+
modal.overlay.classList.remove('is-visible');
|
|
1160
|
+
pendingCreateDetail = null;
|
|
1161
|
+
document.removeEventListener('keydown', handleCreateModalEscape, true);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function confirmCreateLauncherModal() {
|
|
1165
|
+
if (!createLauncherModal || !pendingCreateDetail) return;
|
|
1166
|
+
const folderName = createLauncherModal.input.value.trim();
|
|
1167
|
+
if (!folderName) {
|
|
1168
|
+
createLauncherModal.error.textContent = 'Please enter a folder name.';
|
|
1169
|
+
createLauncherModal.input.focus();
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const prompt = pendingCreateDetail.prompt || '';
|
|
1174
|
+
const redirectUrl = `/pro?name=${encodeURIComponent(folderName)}&message=${encodeURIComponent(prompt)}`;
|
|
1175
|
+
hideCreateLauncherModal();
|
|
1176
|
+
window.location.href = redirectUrl;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function handleCreateModalKeydown(event) {
|
|
1180
|
+
if (event.key === 'Enter') {
|
|
1181
|
+
event.preventDefault();
|
|
1182
|
+
confirmCreateLauncherModal();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function handleCreateModalEscape(event) {
|
|
1187
|
+
if (event.key === 'Escape' && pendingCreateDetail) {
|
|
1188
|
+
event.preventDefault();
|
|
1189
|
+
hideCreateLauncherModal();
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function getCreateLauncherModal() {
|
|
1194
|
+
if (createLauncherModal) {
|
|
1195
|
+
return createLauncherModal;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const overlay = document.createElement('div');
|
|
1199
|
+
overlay.className = 'modal-overlay create-launcher-modal-overlay';
|
|
1200
|
+
|
|
1201
|
+
const modalContent = document.createElement('div');
|
|
1202
|
+
modalContent.className = 'create-launcher-modal';
|
|
1203
|
+
modalContent.setAttribute('role', 'dialog');
|
|
1204
|
+
modalContent.setAttribute('aria-modal', 'true');
|
|
1205
|
+
|
|
1206
|
+
const title = document.createElement('h3');
|
|
1207
|
+
title.id = 'quick-create-launcher-title';
|
|
1208
|
+
title.textContent = 'Create';
|
|
1209
|
+
|
|
1210
|
+
const description = document.createElement('p');
|
|
1211
|
+
description.className = 'create-launcher-modal-description';
|
|
1212
|
+
description.id = 'quick-create-launcher-description';
|
|
1213
|
+
description.textContent = 'Enter a prompt in the search bar to describe your launcher.';
|
|
1214
|
+
|
|
1215
|
+
modalContent.setAttribute('aria-labelledby', title.id);
|
|
1216
|
+
modalContent.setAttribute('aria-describedby', description.id);
|
|
1217
|
+
|
|
1218
|
+
const label = document.createElement('label');
|
|
1219
|
+
label.className = 'create-launcher-modal-label';
|
|
1220
|
+
label.textContent = 'Folder name';
|
|
1221
|
+
|
|
1222
|
+
const input = document.createElement('input');
|
|
1223
|
+
input.type = 'text';
|
|
1224
|
+
input.className = 'create-launcher-modal-input';
|
|
1225
|
+
input.placeholder = 'example: my-launcher';
|
|
1226
|
+
|
|
1227
|
+
const error = document.createElement('div');
|
|
1228
|
+
error.className = 'create-launcher-modal-error';
|
|
1229
|
+
|
|
1230
|
+
const actions = document.createElement('div');
|
|
1231
|
+
actions.className = 'create-launcher-modal-actions';
|
|
1232
|
+
|
|
1233
|
+
const cancelButton = document.createElement('button');
|
|
1234
|
+
cancelButton.type = 'button';
|
|
1235
|
+
cancelButton.className = 'create-launcher-modal-button cancel';
|
|
1236
|
+
cancelButton.textContent = 'Cancel';
|
|
1237
|
+
|
|
1238
|
+
const confirmButton = document.createElement('button');
|
|
1239
|
+
confirmButton.type = 'button';
|
|
1240
|
+
confirmButton.className = 'create-launcher-modal-button confirm';
|
|
1241
|
+
confirmButton.textContent = 'Create';
|
|
1242
|
+
|
|
1243
|
+
actions.appendChild(cancelButton);
|
|
1244
|
+
actions.appendChild(confirmButton);
|
|
1245
|
+
|
|
1246
|
+
label.appendChild(input);
|
|
1247
|
+
modalContent.appendChild(title);
|
|
1248
|
+
modalContent.appendChild(description);
|
|
1249
|
+
modalContent.appendChild(label);
|
|
1250
|
+
modalContent.appendChild(error);
|
|
1251
|
+
modalContent.appendChild(actions);
|
|
1252
|
+
overlay.appendChild(modalContent);
|
|
1253
|
+
document.body.appendChild(overlay);
|
|
1254
|
+
|
|
1255
|
+
overlay.addEventListener('click', function(event) {
|
|
1256
|
+
if (event.target === overlay) {
|
|
1257
|
+
hideCreateLauncherModal();
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
cancelButton.addEventListener('click', hideCreateLauncherModal);
|
|
1262
|
+
confirmButton.addEventListener('click', confirmCreateLauncherModal);
|
|
1263
|
+
input.addEventListener('keydown', handleCreateModalKeydown);
|
|
1264
|
+
|
|
1265
|
+
createLauncherModal = {
|
|
1266
|
+
overlay,
|
|
1267
|
+
modal: modalContent,
|
|
1268
|
+
input,
|
|
1269
|
+
cancelButton,
|
|
1270
|
+
confirmButton,
|
|
1271
|
+
error,
|
|
1272
|
+
description
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
return createLauncherModal;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function generateFolderName(prompt) {
|
|
1279
|
+
if (!prompt) return '';
|
|
1280
|
+
const normalized = prompt
|
|
1281
|
+
.toLowerCase()
|
|
1282
|
+
.replace(/[^a-z0-9\-\s_]/g, '')
|
|
1283
|
+
.replace(/[\s_]+/g, '-');
|
|
1284
|
+
return normalized.replace(/^-+|-+$/g, '').slice(0, 50);
|
|
1285
|
+
}
|
|
636
1286
|
}
|
|
637
1287
|
|
|
638
1288
|
// Auto-initialize if DOM is already loaded, otherwise wait for it
|