pinokiod 3.48.0 → 3.50.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.
@@ -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 = '&times;';
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
+ };
@@ -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
- sessionStorage.setItem(frame_key + ":url", _url.pathname + _url.search + _url.hash)
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 = sessionStorage.getItem(frame_key + ":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()