pinokiod 3.47.0 → 3.49.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/environment.js +6 -0
- package/package.json +1 -1
- package/server/index.js +37 -13
- package/server/public/common.js +85 -8
- package/server/public/style.css +15 -27
- package/server/public/urldropdown.css +174 -0
- package/server/public/urldropdown.js +396 -0
- package/server/public/window_storage.js +30 -0
- package/server/views/app.ejs +19 -27
- package/server/views/columns.ejs +53 -21
- package/server/views/connect.ejs +21 -1
- package/server/views/container.ejs +21 -1
- package/server/views/download.ejs +2 -0
- package/server/views/index.ejs +20 -1
- package/server/views/init/index.ejs +4 -0
- package/server/views/net.ejs +21 -1
- package/server/views/network.ejs +21 -1
- package/server/views/review.ejs +8 -0
- package/server/views/rows.ejs +48 -16
- package/server/views/screenshots.ejs +21 -1
- package/server/views/settings.ejs +22 -2
- package/server/views/setup.ejs +2 -0
- package/server/views/tools.ejs +21 -1
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL Dropdown functionality for process selection
|
|
3
|
+
* Fetches running processes from /info/procs API and displays them in a dropdown
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function initUrlDropdown(config = {}) {
|
|
7
|
+
const urlInput = document.querySelector('.urlbar input[type="url"]');
|
|
8
|
+
const dropdown = document.getElementById('url-dropdown');
|
|
9
|
+
const mobileButton = document.getElementById('mobile-link-button');
|
|
10
|
+
|
|
11
|
+
if (!urlInput || !dropdown) {
|
|
12
|
+
console.warn('URL dropdown elements not found');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Configuration options
|
|
17
|
+
const options = {
|
|
18
|
+
clearBehavior: config.clearBehavior || 'empty', // 'empty' or 'restore'
|
|
19
|
+
defaultValue: config.defaultValue || '',
|
|
20
|
+
apiEndpoint: config.apiEndpoint || '/info/procs',
|
|
21
|
+
...config
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let isDropdownVisible = false;
|
|
25
|
+
let allProcesses = []; // Store all processes for filtering
|
|
26
|
+
let filteredProcesses = []; // Store currently filtered processes
|
|
27
|
+
|
|
28
|
+
// Initialize input field state based on clear behavior
|
|
29
|
+
initializeInputValue();
|
|
30
|
+
|
|
31
|
+
// Handle page navigation events
|
|
32
|
+
window.addEventListener('pageshow', function(event) {
|
|
33
|
+
if (event.persisted || window.performance?.navigation?.type === 2) {
|
|
34
|
+
initializeInputValue();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Event listeners
|
|
39
|
+
urlInput.addEventListener('focus', function() {
|
|
40
|
+
// Auto-select text for restore behavior to make filtering easier
|
|
41
|
+
if (options.clearBehavior === 'restore' && urlInput.value) {
|
|
42
|
+
// Use setTimeout to ensure the focus event completes first
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
urlInput.select();
|
|
45
|
+
}, 0);
|
|
46
|
+
}
|
|
47
|
+
showDropdown();
|
|
48
|
+
});
|
|
49
|
+
urlInput.addEventListener('input', handleInputChange);
|
|
50
|
+
|
|
51
|
+
// Hide dropdown when clicking outside
|
|
52
|
+
document.addEventListener('click', function(e) {
|
|
53
|
+
if (!e.target.closest('.url-input-container')) {
|
|
54
|
+
hideDropdown();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function initializeInputValue() {
|
|
59
|
+
if (options.clearBehavior === 'empty') {
|
|
60
|
+
urlInput.value = '';
|
|
61
|
+
} else if (options.clearBehavior === 'restore') {
|
|
62
|
+
const originalValue = urlInput.getAttribute('value') || options.defaultValue;
|
|
63
|
+
if (urlInput.value !== originalValue) {
|
|
64
|
+
urlInput.value = originalValue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function showDropdown() {
|
|
70
|
+
if (isDropdownVisible && allProcesses.length > 0) {
|
|
71
|
+
// If dropdown is already visible and we have data, show all initially
|
|
72
|
+
showAllProcesses();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
isDropdownVisible = true;
|
|
77
|
+
dropdown.style.display = 'block';
|
|
78
|
+
|
|
79
|
+
// If we already have processes data, show all initially
|
|
80
|
+
if (allProcesses.length > 0) {
|
|
81
|
+
showAllProcesses();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Otherwise, show loading and fetch data
|
|
86
|
+
dropdown.innerHTML = '<div class="url-dropdown-loading">Loading running processes...</div>';
|
|
87
|
+
|
|
88
|
+
// Fetch processes from API
|
|
89
|
+
fetch(options.apiEndpoint)
|
|
90
|
+
.then(response => {
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
93
|
+
}
|
|
94
|
+
return response.json();
|
|
95
|
+
})
|
|
96
|
+
.then(data => {
|
|
97
|
+
allProcesses = data.info || [];
|
|
98
|
+
showAllProcesses(); // Show all processes when dropdown first opens
|
|
99
|
+
})
|
|
100
|
+
.catch(error => {
|
|
101
|
+
console.error('Failed to fetch processes:', error);
|
|
102
|
+
dropdown.innerHTML = '<div class="url-dropdown-empty">Failed to load processes</div>';
|
|
103
|
+
allProcesses = [];
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function showAllProcesses() {
|
|
108
|
+
filteredProcesses = allProcesses;
|
|
109
|
+
populateDropdown(filteredProcesses);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function handleInputChange() {
|
|
113
|
+
if (!isDropdownVisible) return;
|
|
114
|
+
|
|
115
|
+
const query = urlInput.value.toLowerCase().trim();
|
|
116
|
+
|
|
117
|
+
// Special case: if text is selected (user just focused), don't filter yet
|
|
118
|
+
if (urlInput.selectionStart === 0 && urlInput.selectionEnd === urlInput.value.length) {
|
|
119
|
+
// Text is fully selected, show all processes until user starts typing
|
|
120
|
+
filteredProcesses = allProcesses;
|
|
121
|
+
} else if (!query) {
|
|
122
|
+
// No query, show all processes
|
|
123
|
+
filteredProcesses = allProcesses;
|
|
124
|
+
} else {
|
|
125
|
+
// Filter processes based on name and URL
|
|
126
|
+
filteredProcesses = allProcesses.filter(process => {
|
|
127
|
+
const url = `http://${process.ip}`;
|
|
128
|
+
const name = process.name.toLowerCase();
|
|
129
|
+
const urlLower = url.toLowerCase();
|
|
130
|
+
|
|
131
|
+
return name.includes(query) || urlLower.includes(query);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
populateDropdown(filteredProcesses);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function hideDropdown() {
|
|
139
|
+
isDropdownVisible = false;
|
|
140
|
+
dropdown.style.display = 'none';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function populateDropdown(processes) {
|
|
144
|
+
if (processes.length === 0) {
|
|
145
|
+
const query = urlInput.value.toLowerCase().trim();
|
|
146
|
+
const message = query
|
|
147
|
+
? `No processes match "${query}"`
|
|
148
|
+
: 'No running processes found';
|
|
149
|
+
dropdown.innerHTML = `<div class="url-dropdown-empty">${message}</div>`;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const items = processes.map(process => {
|
|
154
|
+
const url = `http://${process.ip}`;
|
|
155
|
+
return `
|
|
156
|
+
<div class="url-dropdown-item" data-url="${url}">
|
|
157
|
+
<div class="url-dropdown-name">${escapeHtml(process.name)}</div>
|
|
158
|
+
<div class="url-dropdown-url">${escapeHtml(url)}</div>
|
|
159
|
+
</div>
|
|
160
|
+
`;
|
|
161
|
+
}).join('');
|
|
162
|
+
|
|
163
|
+
dropdown.innerHTML = items;
|
|
164
|
+
|
|
165
|
+
// Add click handlers to dropdown items
|
|
166
|
+
dropdown.querySelectorAll('.url-dropdown-item').forEach(item => {
|
|
167
|
+
item.addEventListener('click', function() {
|
|
168
|
+
const url = this.getAttribute('data-url');
|
|
169
|
+
urlInput.value = url;
|
|
170
|
+
hideDropdown();
|
|
171
|
+
// Submit the form
|
|
172
|
+
urlInput.closest('form').dispatchEvent(new Event('submit'));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Utility function to escape HTML
|
|
178
|
+
function escapeHtml(text) {
|
|
179
|
+
const div = document.createElement('div');
|
|
180
|
+
div.textContent = text;
|
|
181
|
+
return div.innerHTML;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Mobile modal functionality
|
|
185
|
+
function createMobileModal() {
|
|
186
|
+
const overlay = document.createElement('div');
|
|
187
|
+
overlay.className = 'url-modal-overlay';
|
|
188
|
+
overlay.id = 'url-modal-overlay';
|
|
189
|
+
|
|
190
|
+
const content = document.createElement('div');
|
|
191
|
+
content.className = 'url-modal-content';
|
|
192
|
+
|
|
193
|
+
const closeButton = document.createElement('span');
|
|
194
|
+
closeButton.className = 'url-modal-close';
|
|
195
|
+
closeButton.innerHTML = '×';
|
|
196
|
+
closeButton.onclick = closeMobileModal;
|
|
197
|
+
|
|
198
|
+
const modalInput = document.createElement('input');
|
|
199
|
+
modalInput.type = 'url';
|
|
200
|
+
modalInput.className = 'url-modal-input';
|
|
201
|
+
modalInput.placeholder = 'enter a local url';
|
|
202
|
+
|
|
203
|
+
const modalDropdown = document.createElement('div');
|
|
204
|
+
modalDropdown.className = 'url-dropdown';
|
|
205
|
+
modalDropdown.id = 'url-modal-dropdown';
|
|
206
|
+
modalDropdown.style.position = 'relative';
|
|
207
|
+
modalDropdown.style.top = '0';
|
|
208
|
+
modalDropdown.style.left = '0';
|
|
209
|
+
modalDropdown.style.right = '0';
|
|
210
|
+
modalDropdown.style.marginTop = '10px';
|
|
211
|
+
|
|
212
|
+
content.appendChild(closeButton);
|
|
213
|
+
content.appendChild(modalInput);
|
|
214
|
+
content.appendChild(modalDropdown);
|
|
215
|
+
overlay.appendChild(content);
|
|
216
|
+
|
|
217
|
+
return { overlay, input: modalInput, dropdown: modalDropdown };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function showMobileModal() {
|
|
221
|
+
let modal = document.getElementById('url-modal-overlay');
|
|
222
|
+
if (!modal) {
|
|
223
|
+
const { overlay, input: modalInput, dropdown: modalDropdown } = createMobileModal();
|
|
224
|
+
modal = overlay;
|
|
225
|
+
document.body.appendChild(modal);
|
|
226
|
+
|
|
227
|
+
// Initialize dropdown functionality for modal
|
|
228
|
+
modalInput.addEventListener('focus', function() {
|
|
229
|
+
if (options.clearBehavior === 'restore' && modalInput.value) {
|
|
230
|
+
setTimeout(() => modalInput.select(), 0);
|
|
231
|
+
}
|
|
232
|
+
showModalDropdown(modalDropdown);
|
|
233
|
+
});
|
|
234
|
+
modalInput.addEventListener('input', function() {
|
|
235
|
+
handleModalInputChange(modalInput, modalDropdown);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Close modal when clicking outside content
|
|
239
|
+
modal.addEventListener('click', function(e) {
|
|
240
|
+
if (e.target === modal) {
|
|
241
|
+
closeMobileModal();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Handle form submission
|
|
246
|
+
modalInput.addEventListener('keypress', function(e) {
|
|
247
|
+
if (e.key === 'Enter') {
|
|
248
|
+
e.preventDefault();
|
|
249
|
+
if (modalInput.value) {
|
|
250
|
+
urlInput.value = modalInput.value;
|
|
251
|
+
urlInput.closest('form').dispatchEvent(new Event('submit'));
|
|
252
|
+
closeMobileModal();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
modal.style.display = 'flex';
|
|
259
|
+
const modalInput = modal.querySelector('.url-modal-input');
|
|
260
|
+
|
|
261
|
+
// Set initial value based on config
|
|
262
|
+
if (options.clearBehavior === 'restore') {
|
|
263
|
+
modalInput.value = urlInput.value || options.defaultValue || '';
|
|
264
|
+
} else {
|
|
265
|
+
modalInput.value = '';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
setTimeout(() => modalInput.focus(), 100);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function closeMobileModal() {
|
|
272
|
+
const modal = document.getElementById('url-modal-overlay');
|
|
273
|
+
if (modal) {
|
|
274
|
+
modal.style.display = 'none';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function showModalDropdown(modalDropdown) {
|
|
279
|
+
modalDropdown.style.display = 'block';
|
|
280
|
+
|
|
281
|
+
if (allProcesses.length > 0) {
|
|
282
|
+
populateModalDropdown(allProcesses, modalDropdown);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
modalDropdown.innerHTML = '<div class="url-dropdown-loading">Loading running processes...</div>';
|
|
287
|
+
|
|
288
|
+
fetch(options.apiEndpoint)
|
|
289
|
+
.then(response => {
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
292
|
+
}
|
|
293
|
+
return response.json();
|
|
294
|
+
})
|
|
295
|
+
.then(data => {
|
|
296
|
+
allProcesses = data.info || [];
|
|
297
|
+
populateModalDropdown(allProcesses, modalDropdown);
|
|
298
|
+
})
|
|
299
|
+
.catch(error => {
|
|
300
|
+
console.error('Failed to fetch processes:', error);
|
|
301
|
+
modalDropdown.innerHTML = '<div class="url-dropdown-empty">Failed to load processes</div>';
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function handleModalInputChange(modalInput, modalDropdown) {
|
|
306
|
+
const query = modalInput.value.toLowerCase().trim();
|
|
307
|
+
let filtered = allProcesses;
|
|
308
|
+
|
|
309
|
+
if (modalInput.selectionStart === 0 && modalInput.selectionEnd === modalInput.value.length) {
|
|
310
|
+
filtered = allProcesses;
|
|
311
|
+
} else if (query) {
|
|
312
|
+
filtered = allProcesses.filter(process => {
|
|
313
|
+
const url = `http://${process.ip}`;
|
|
314
|
+
const name = process.name.toLowerCase();
|
|
315
|
+
const urlLower = url.toLowerCase();
|
|
316
|
+
return name.includes(query) || urlLower.includes(query);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
populateModalDropdown(filtered, modalDropdown);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function populateModalDropdown(processes, modalDropdown) {
|
|
324
|
+
const modalInput = modalDropdown.parentElement.querySelector('.url-modal-input');
|
|
325
|
+
|
|
326
|
+
if (processes.length === 0) {
|
|
327
|
+
const query = modalInput.value.toLowerCase().trim();
|
|
328
|
+
const message = query ? `No processes match "${query}"` : 'No running processes found';
|
|
329
|
+
modalDropdown.innerHTML = `<div class="url-dropdown-empty">${message}</div>`;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const items = processes.map(process => {
|
|
334
|
+
const url = `http://${process.ip}`;
|
|
335
|
+
return `
|
|
336
|
+
<div class="url-dropdown-item" data-url="${url}">
|
|
337
|
+
<div class="url-dropdown-name">${escapeHtml(process.name)}</div>
|
|
338
|
+
<div class="url-dropdown-url">${escapeHtml(url)}</div>
|
|
339
|
+
</div>
|
|
340
|
+
`;
|
|
341
|
+
}).join('');
|
|
342
|
+
|
|
343
|
+
modalDropdown.innerHTML = items;
|
|
344
|
+
|
|
345
|
+
modalDropdown.querySelectorAll('.url-dropdown-item').forEach(item => {
|
|
346
|
+
item.addEventListener('click', function() {
|
|
347
|
+
const url = this.getAttribute('data-url');
|
|
348
|
+
modalInput.value = url;
|
|
349
|
+
urlInput.value = url;
|
|
350
|
+
urlInput.closest('form').dispatchEvent(new Event('submit'));
|
|
351
|
+
closeMobileModal();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Set up mobile button click handler
|
|
357
|
+
if (mobileButton) {
|
|
358
|
+
mobileButton.addEventListener('click', showMobileModal);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Public API
|
|
362
|
+
return {
|
|
363
|
+
show: showDropdown,
|
|
364
|
+
hide: hideDropdown,
|
|
365
|
+
showAll: showAllProcesses,
|
|
366
|
+
showMobileModal: showMobileModal,
|
|
367
|
+
closeMobileModal: closeMobileModal,
|
|
368
|
+
refresh: function() {
|
|
369
|
+
allProcesses = []; // Clear cache to force refetch
|
|
370
|
+
if (isDropdownVisible) {
|
|
371
|
+
showDropdown();
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
filter: handleInputChange,
|
|
375
|
+
destroy: function() {
|
|
376
|
+
// Remove the focus event listener (need to store reference)
|
|
377
|
+
urlInput.removeEventListener('input', handleInputChange);
|
|
378
|
+
if (mobileButton) {
|
|
379
|
+
mobileButton.removeEventListener('click', showMobileModal);
|
|
380
|
+
}
|
|
381
|
+
hideDropdown();
|
|
382
|
+
closeMobileModal();
|
|
383
|
+
allProcesses = [];
|
|
384
|
+
filteredProcesses = [];
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Auto-initialize if DOM is already loaded, otherwise wait for it
|
|
390
|
+
if (document.readyState === 'loading') {
|
|
391
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
392
|
+
// Will be initialized by individual templates with their specific config
|
|
393
|
+
});
|
|
394
|
+
} else {
|
|
395
|
+
// DOM is already loaded, templates can initialize immediately
|
|
396
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const WINDOW_ID = (() => {
|
|
2
|
+
// Try to get existing window ID or create a new one
|
|
3
|
+
let id = sessionStorage.getItem('__window_id');
|
|
4
|
+
if (!id) {
|
|
5
|
+
id = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
6
|
+
try { sessionStorage.setItem('__window_id', id); } catch (_) {}
|
|
7
|
+
}
|
|
8
|
+
return id;
|
|
9
|
+
})();
|
|
10
|
+
|
|
11
|
+
// Window-specific storage wrapper
|
|
12
|
+
window.windowStorage = {
|
|
13
|
+
setItem: (key, value) => {
|
|
14
|
+
try {
|
|
15
|
+
sessionStorage.setItem(`${WINDOW_ID}:${key}`, value);
|
|
16
|
+
} catch (_) {}
|
|
17
|
+
},
|
|
18
|
+
removeItem: (key, value) => {
|
|
19
|
+
try {
|
|
20
|
+
sessionStorage.removeItem(`${WINDOW_ID}:${key}`)
|
|
21
|
+
} catch (_) {}
|
|
22
|
+
},
|
|
23
|
+
getItem: (key) => {
|
|
24
|
+
try {
|
|
25
|
+
return sessionStorage.getItem(`${WINDOW_ID}:${key}`);
|
|
26
|
+
} catch (_) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
package/server/views/app.ejs
CHANGED
|
@@ -1663,13 +1663,30 @@ body.dark .pinokio-git-commit-hash:hover {
|
|
|
1663
1663
|
body.minimized {
|
|
1664
1664
|
flex-direction: row !important;
|
|
1665
1665
|
}
|
|
1666
|
+
body.minimized aside {
|
|
1667
|
+
display: none;
|
|
1668
|
+
}
|
|
1669
|
+
@media only screen and (max-width: 800px) {
|
|
1670
|
+
.mode-selector .btn2 {
|
|
1671
|
+
width: unset;
|
|
1672
|
+
}
|
|
1673
|
+
.mode-selector .btn2 .caption {
|
|
1674
|
+
display: none;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
/*
|
|
1666
1678
|
@media only screen and (max-width: 800px) {
|
|
1667
1679
|
body {
|
|
1668
1680
|
flex-direction: row !important;
|
|
1669
1681
|
}
|
|
1682
|
+
aside {
|
|
1683
|
+
display: none;
|
|
1684
|
+
}
|
|
1670
1685
|
}
|
|
1686
|
+
*/
|
|
1671
1687
|
</style>
|
|
1672
1688
|
<link href="/app.css" rel="stylesheet"/>
|
|
1689
|
+
<script src="/window_storage.js"></script>
|
|
1673
1690
|
<script src="/timeago.min.js"></script>
|
|
1674
1691
|
<script src="/hotkeys.min.js"></script>
|
|
1675
1692
|
<script src="/sweetalert2.js"></script>
|
|
@@ -2003,24 +2020,6 @@ body.minimized {
|
|
|
2003
2020
|
document.querySelector("main").appendChild(frame)
|
|
2004
2021
|
loaded[name] = true
|
|
2005
2022
|
}
|
|
2006
|
-
/*
|
|
2007
|
-
if (document.body.classList.contains("minimized")) {
|
|
2008
|
-
document.querySelector("#collapse i").className = "fa-solid fa-compress"
|
|
2009
|
-
} else {
|
|
2010
|
-
document.querySelector("#collapse i").className = "fa-solid fa-expand"
|
|
2011
|
-
}
|
|
2012
|
-
*/
|
|
2013
|
-
document.querySelector("#collapse").addEventListener("click", (e) => {
|
|
2014
|
-
document.body.classList.toggle("minimized")
|
|
2015
|
-
let frame_key = window.frameElement?.name || "";
|
|
2016
|
-
if (document.body.classList.contains("minimized")) {
|
|
2017
|
-
// document.querySelector("#collapse i").className = "fa-solid fa-compress"
|
|
2018
|
-
sessionStorage.setItem(frame_key + ":window_mode", "minimized")
|
|
2019
|
-
} else {
|
|
2020
|
-
// document.querySelector("#collapse i").className = "fa-solid fa-expand"
|
|
2021
|
-
sessionStorage.setItem(frame_key + ":window_mode", "full")
|
|
2022
|
-
}
|
|
2023
|
-
})
|
|
2024
2023
|
document.addEventListener("click", (e) => {
|
|
2025
2024
|
interacted = true
|
|
2026
2025
|
})
|
|
@@ -2256,7 +2255,7 @@ body.minimized {
|
|
|
2256
2255
|
<% if (type !== "run") { %>
|
|
2257
2256
|
let _url = new URL(target.href)
|
|
2258
2257
|
let frame_key = window.frameElement?.name || "";
|
|
2259
|
-
|
|
2258
|
+
windowStorage.setItem(frame_key + ":url", _url.pathname + _url.search + _url.hash)
|
|
2260
2259
|
<% } %>
|
|
2261
2260
|
|
|
2262
2261
|
// hide all frames
|
|
@@ -3223,7 +3222,7 @@ body.minimized {
|
|
|
3223
3222
|
refresh_du("logs")
|
|
3224
3223
|
let frame_key = window.frameElement?.name || "";
|
|
3225
3224
|
|
|
3226
|
-
let selection_url =
|
|
3225
|
+
let selection_url = windowStorage.getItem(frame_key + ":url")
|
|
3227
3226
|
console.log({ frame_key, selection_url })
|
|
3228
3227
|
let selection
|
|
3229
3228
|
if (selection_url) {
|
|
@@ -3237,13 +3236,6 @@ body.minimized {
|
|
|
3237
3236
|
selection.click()
|
|
3238
3237
|
// }, 100)
|
|
3239
3238
|
}
|
|
3240
|
-
let window_mode = sessionStorage.getItem(frame_key + ":window_mode")
|
|
3241
|
-
console.log({ window_mode })
|
|
3242
|
-
if (window_mode) {
|
|
3243
|
-
if (window_mode === "minimized") {
|
|
3244
|
-
document.body.classList.add("minimized")
|
|
3245
|
-
}
|
|
3246
|
-
}
|
|
3247
3239
|
<% if (type !== 'run') { %>
|
|
3248
3240
|
fetch("<%=repos%>").then((res) => {
|
|
3249
3241
|
return res.text()
|
package/server/views/columns.ejs
CHANGED
|
@@ -54,6 +54,7 @@ body.resizing {
|
|
|
54
54
|
.gutter:hover::before, body.resizing .gutter::before { background: #9e9e9e; }
|
|
55
55
|
.gutter:focus { outline: none; box-shadow: inset 0 0 0 2px #90caf9; }
|
|
56
56
|
</style>
|
|
57
|
+
<script src="/window_storage.js"></script>
|
|
57
58
|
</head>
|
|
58
59
|
<body class='<%=theme%>'>
|
|
59
60
|
<iframe id='col0' data-src="<%=src%>"></iframe>
|
|
@@ -103,7 +104,7 @@ body.resizing {
|
|
|
103
104
|
const total = computeTotal();
|
|
104
105
|
if (total > 0) {
|
|
105
106
|
const ratio = clamp(leftPx / total, 0, 1);
|
|
106
|
-
|
|
107
|
+
windowStorage.setItem(splitKey, String(ratio));
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -118,7 +119,7 @@ body.resizing {
|
|
|
118
119
|
// --- Per-window URL persistence for each pane ---
|
|
119
120
|
function restorePaneURL(pane, key) {
|
|
120
121
|
try {
|
|
121
|
-
const saved =
|
|
122
|
+
const saved = windowStorage.getItem(key);
|
|
122
123
|
const fallback = pane.getAttribute('data-src') || pane.getAttribute('src') || '';
|
|
123
124
|
const target = (saved && typeof saved === 'string') ? saved : fallback;
|
|
124
125
|
if (target && pane.src !== target) pane.src = target;
|
|
@@ -130,7 +131,7 @@ body.resizing {
|
|
|
130
131
|
const cw = pane.contentWindow;
|
|
131
132
|
if (!cw) return;
|
|
132
133
|
const notify = () => {
|
|
133
|
-
|
|
134
|
+
windowStorage.setItem(key, cw.location.href);
|
|
134
135
|
};
|
|
135
136
|
// Hook SPA navigations
|
|
136
137
|
const _ps = cw.history.pushState;
|
|
@@ -143,7 +144,7 @@ body.resizing {
|
|
|
143
144
|
else notify();
|
|
144
145
|
} catch (err) {
|
|
145
146
|
// Cross-origin: fall back to saving src only
|
|
146
|
-
|
|
147
|
+
windowStorage.setItem(key, pane.src);
|
|
147
148
|
}
|
|
148
149
|
}
|
|
149
150
|
function onPaneLoadFactory(pane, key) {
|
|
@@ -170,8 +171,10 @@ body.resizing {
|
|
|
170
171
|
let overlay = null;
|
|
171
172
|
|
|
172
173
|
function refreshLayout (splitKey) {
|
|
173
|
-
|
|
174
|
+
console.log("refreshLayout", splitKey)
|
|
175
|
+
let val = windowStorage.getItem(splitKey)
|
|
174
176
|
let id = splitKey.replace("splitRatio:", "")
|
|
177
|
+
console.log({ id, val })
|
|
175
178
|
if (val === "1" || val === "0") {
|
|
176
179
|
if (val === "1") {
|
|
177
180
|
id_to_hide = id + ".1"
|
|
@@ -180,8 +183,28 @@ body.resizing {
|
|
|
180
183
|
}
|
|
181
184
|
const el = document.querySelector(`iframe[name='${id_to_hide}']`)
|
|
182
185
|
el.remove()
|
|
183
|
-
document.
|
|
184
|
-
|
|
186
|
+
if (document.querySelector("#gutter")) {
|
|
187
|
+
document.querySelector("#gutter").remove()
|
|
188
|
+
}
|
|
189
|
+
let existing_iframe = document.querySelector("iframe")
|
|
190
|
+
console.log("1")
|
|
191
|
+
if (existing_iframe) {
|
|
192
|
+
console.log("2")
|
|
193
|
+
document.body.className = "single"
|
|
194
|
+
} else {
|
|
195
|
+
console.log("3")
|
|
196
|
+
if (window.parent) {
|
|
197
|
+
console.log("4")
|
|
198
|
+
// if all child iframes have been removed, remove self
|
|
199
|
+
window.parent.postMessage({
|
|
200
|
+
e: "close"
|
|
201
|
+
}, "*")
|
|
202
|
+
} else {
|
|
203
|
+
console.log("5")
|
|
204
|
+
// if this is the top window, everything has been removed, so just redirect to home
|
|
205
|
+
location.href = "/"
|
|
206
|
+
}
|
|
207
|
+
}
|
|
185
208
|
}
|
|
186
209
|
}
|
|
187
210
|
|
|
@@ -262,7 +285,7 @@ body.resizing {
|
|
|
262
285
|
|
|
263
286
|
// Initialize from saved ratio if available and set ARIA
|
|
264
287
|
try {
|
|
265
|
-
const saved = parseFloat(
|
|
288
|
+
const saved = parseFloat(windowStorage.getItem(splitKey) || '');
|
|
266
289
|
console.log({ saved })
|
|
267
290
|
if (!Number.isNaN(saved) && saved > 0 && saved < 1) {
|
|
268
291
|
console.log("> 1")
|
|
@@ -280,7 +303,7 @@ body.resizing {
|
|
|
280
303
|
// Re-apply on window resize to keep ratio
|
|
281
304
|
window.addEventListener('resize', () => {
|
|
282
305
|
try {
|
|
283
|
-
const saved = parseFloat(
|
|
306
|
+
const saved = parseFloat(windowStorage.getItem(splitKey) || '');
|
|
284
307
|
if (!Number.isNaN(saved) && saved > 0 && saved < 1) {
|
|
285
308
|
applyFromRatio(saved);
|
|
286
309
|
} else {
|
|
@@ -299,11 +322,17 @@ body.resizing {
|
|
|
299
322
|
|
|
300
323
|
let sourceFrameId = null;
|
|
301
324
|
|
|
302
|
-
if (event.source === col0.contentWindow) {
|
|
325
|
+
if (col0 && event.source === col0.contentWindow) {
|
|
303
326
|
sourceFrameId = 'col0';
|
|
304
|
-
} else if (event.source === col1.contentWindow) {
|
|
327
|
+
} else if (col1 && event.source === col1.contentWindow) {
|
|
305
328
|
sourceFrameId = 'col1';
|
|
306
329
|
}
|
|
330
|
+
|
|
331
|
+
if (!sourceFrameId) {
|
|
332
|
+
windowStorage.removeItem(splitKey)
|
|
333
|
+
location.href = "/"
|
|
334
|
+
return
|
|
335
|
+
}
|
|
307
336
|
|
|
308
337
|
console.log('Message received from iframe:', sourceFrameId);
|
|
309
338
|
|
|
@@ -312,19 +341,11 @@ body.resizing {
|
|
|
312
341
|
console.log({ splitKey })
|
|
313
342
|
for (let iframe of iframes) {
|
|
314
343
|
if (event.source === iframe.contentWindow) {
|
|
315
|
-
// const splitKey = `splitRatio:${iframe.name}`
|
|
316
|
-
// console.log({ splitKey })
|
|
317
344
|
if (iframe.id === "col0") {
|
|
318
|
-
|
|
319
|
-
// col0.src = "about:blank"
|
|
320
|
-
// col0.style.display = "none"
|
|
321
|
-
try { sessionStorage.setItem(splitKey, "0"); } catch (_) {}
|
|
345
|
+
windowStorage.setItem(splitKey, "0");
|
|
322
346
|
refreshLayout(splitKey)
|
|
323
347
|
} else if (iframe.id === "col1") {
|
|
324
|
-
|
|
325
|
-
// col1.src = "about:blank"
|
|
326
|
-
// col1.style.display = "none"
|
|
327
|
-
try { sessionStorage.setItem(splitKey, "1"); } catch (_) { console.log("<<< ", _ )}
|
|
348
|
+
windowStorage.setItem(splitKey, "1");
|
|
328
349
|
refreshLayout(splitKey)
|
|
329
350
|
}
|
|
330
351
|
break;
|
|
@@ -333,6 +354,17 @@ body.resizing {
|
|
|
333
354
|
}
|
|
334
355
|
})
|
|
335
356
|
})();
|
|
357
|
+
if (document.querySelector("#collapse") && window.windowStorage) {
|
|
358
|
+
document.querySelector("#collapse").addEventListener("click", (e) => {
|
|
359
|
+
document.body.classList.toggle("minimized")
|
|
360
|
+
let frame_key = window.frameElement?.name || "";
|
|
361
|
+
if (document.body.classList.contains("minimized")) {
|
|
362
|
+
windowStorage.setItem(frame_key + ":window_mode", "minimized")
|
|
363
|
+
} else {
|
|
364
|
+
windowStorage.setItem(frame_key + ":window_mode", "full")
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
}
|
|
336
368
|
</script>
|
|
337
369
|
</body>
|
|
338
370
|
</html>
|