pinokiod 3.180.0 → 3.181.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/favicon.js +91 -34
- package/kernel/peer.js +73 -0
- package/kernel/util.js +13 -2
- package/package.json +1 -1
- package/server/index.js +126 -23
- package/server/public/common.js +244 -0
- package/server/public/layout.js +114 -0
- package/server/public/nav.js +227 -64
- package/server/public/style.css +22 -2
- package/server/public/tab-idle-notifier.js +3 -0
- package/server/socket.js +71 -4
- package/server/views/app.ejs +226 -56
- package/server/views/connect.ejs +9 -0
- package/server/views/index.ejs +9 -0
- package/server/views/init/index.ejs +9 -2
- package/server/views/layout.ejs +2 -0
- package/server/views/net.ejs +9 -0
- package/server/views/network.ejs +9 -0
- package/server/views/screenshots.ejs +9 -0
- package/server/views/settings.ejs +9 -0
- package/server/views/terminals.ejs +12 -3
- package/server/views/tools.ejs +10 -1
package/server/public/nav.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
document.addEventListener("DOMContentLoaded", () => {
|
|
2
|
+
// Logging disabled for production
|
|
3
|
+
const log = () => {};
|
|
4
|
+
const rectInfo = () => null;
|
|
5
|
+
|
|
2
6
|
const newWindowButton = document.querySelector("#new-window");
|
|
3
7
|
if (newWindowButton) {
|
|
4
8
|
newWindowButton.addEventListener("click", (event) => {
|
|
@@ -14,10 +18,70 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
14
18
|
const header = document.querySelector("header.navheader");
|
|
15
19
|
const minimizeButton = document.querySelector("#minimize-header");
|
|
16
20
|
const homeLink = header ? header.querySelector(".home") : null;
|
|
17
|
-
|
|
21
|
+
log("init:elements", { hasHeader: !!header, hasMinimize: !!minimizeButton, hasHome: !!homeLink });
|
|
22
|
+
// Only require the header; other controls may be missing on some views
|
|
23
|
+
if (!header) {
|
|
24
|
+
log("init:abort:no-header", {});
|
|
18
25
|
return;
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
// Helper functions used during initial restore must be defined before use
|
|
29
|
+
const MIN_MARGIN = 8;
|
|
30
|
+
|
|
31
|
+
function clampPosition(left, top, sizeOverride) {
|
|
32
|
+
const rect = header.getBoundingClientRect();
|
|
33
|
+
const width = sizeOverride && Number.isFinite(sizeOverride.width) ? sizeOverride.width : rect.width;
|
|
34
|
+
const height = sizeOverride && Number.isFinite(sizeOverride.height) ? sizeOverride.height : rect.height;
|
|
35
|
+
const maxLeft = Math.max(0, window.innerWidth - width);
|
|
36
|
+
const maxTop = Math.max(0, window.innerHeight - height);
|
|
37
|
+
return {
|
|
38
|
+
left: Math.min(Math.max(0, left), maxLeft),
|
|
39
|
+
top: Math.min(Math.max(0, top), maxTop),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function applyPosition(left, top) {
|
|
44
|
+
header.style.left = `${left}px`;
|
|
45
|
+
header.style.top = `${top}px`;
|
|
46
|
+
header.style.right = "auto";
|
|
47
|
+
header.style.bottom = "auto";
|
|
48
|
+
log("pos:apply", { left, top });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function measureRect(configureClone) {
|
|
52
|
+
const clone = header.cloneNode(true);
|
|
53
|
+
// Avoid duplicate-IDs while preserving #refresh-page so measurement matches visible controls
|
|
54
|
+
clone.querySelectorAll("[id]").forEach((node) => {
|
|
55
|
+
if (node.id !== "refresh-page") {
|
|
56
|
+
node.removeAttribute("id");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
Object.assign(clone.style, {
|
|
60
|
+
transition: "none",
|
|
61
|
+
transform: "none",
|
|
62
|
+
position: "fixed",
|
|
63
|
+
visibility: "hidden",
|
|
64
|
+
pointerEvents: "none",
|
|
65
|
+
margin: "0",
|
|
66
|
+
left: "0",
|
|
67
|
+
top: "0",
|
|
68
|
+
right: "auto",
|
|
69
|
+
bottom: "auto",
|
|
70
|
+
width: "auto",
|
|
71
|
+
height: "auto",
|
|
72
|
+
});
|
|
73
|
+
document.body.appendChild(clone);
|
|
74
|
+
if (typeof configureClone === "function") {
|
|
75
|
+
configureClone(clone);
|
|
76
|
+
}
|
|
77
|
+
clone.style.right = "auto";
|
|
78
|
+
clone.style.bottom = "auto";
|
|
79
|
+
const rect = clone.getBoundingClientRect();
|
|
80
|
+
log("measure", { minimized: !!clone.classList.contains("minimized"), rect: rectInfo(rect) });
|
|
81
|
+
clone.remove();
|
|
82
|
+
return rect;
|
|
83
|
+
}
|
|
84
|
+
|
|
21
85
|
const dispatchHeaderState = (minimized, detail = {}) => {
|
|
22
86
|
if (typeof window === "undefined" || typeof window.CustomEvent !== "function") {
|
|
23
87
|
return;
|
|
@@ -36,8 +100,49 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
36
100
|
dragHandle.setAttribute("aria-hidden", "true");
|
|
37
101
|
dragHandle.setAttribute("title", "Drag minimized header");
|
|
38
102
|
headerTitle.insertBefore(dragHandle, homeLink ? homeLink.nextSibling : headerTitle.firstChild);
|
|
103
|
+
log("init:drag-handle:created", {});
|
|
39
104
|
}
|
|
40
105
|
|
|
106
|
+
const STORAGE_KEY = () => `pinokio:header-state:v1:${location.pathname}`;
|
|
107
|
+
const RESTORE_ONCE_KEY = () => `pinokio:header-restore-once:${location.pathname}`;
|
|
108
|
+
const storage = (() => {
|
|
109
|
+
try { return window.sessionStorage; } catch (_) { return null; }
|
|
110
|
+
})();
|
|
111
|
+
const readPersisted = () => {
|
|
112
|
+
if (!storage) return null;
|
|
113
|
+
try {
|
|
114
|
+
const raw = storage.getItem(STORAGE_KEY());
|
|
115
|
+
if (!raw) return null;
|
|
116
|
+
const data = JSON.parse(raw);
|
|
117
|
+
if (!data || typeof data !== "object") return null;
|
|
118
|
+
const left = Number.isFinite(data.left) ? data.left : null;
|
|
119
|
+
const top = Number.isFinite(data.top) ? data.top : null;
|
|
120
|
+
const minimized = !!data.minimized;
|
|
121
|
+
const out = { minimized, left, top };
|
|
122
|
+
log("storage:read", { key: STORAGE_KEY(), value: out });
|
|
123
|
+
return out;
|
|
124
|
+
} catch (_) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const writePersisted = (data) => {
|
|
129
|
+
if (!storage) return;
|
|
130
|
+
try {
|
|
131
|
+
const prev = readPersisted() || {};
|
|
132
|
+
const next = { ...prev, ...data };
|
|
133
|
+
storage.setItem(STORAGE_KEY(), JSON.stringify(next));
|
|
134
|
+
log("storage:write", { key: STORAGE_KEY(), value: next });
|
|
135
|
+
} catch (_) {}
|
|
136
|
+
};
|
|
137
|
+
const readRestoreOnce = () => {
|
|
138
|
+
if (!storage) return false;
|
|
139
|
+
try {
|
|
140
|
+
const raw = storage.getItem(RESTORE_ONCE_KEY());
|
|
141
|
+
storage.removeItem(RESTORE_ONCE_KEY());
|
|
142
|
+
return raw === "1";
|
|
143
|
+
} catch (_) { return false; }
|
|
144
|
+
};
|
|
145
|
+
|
|
41
146
|
const state = {
|
|
42
147
|
minimized: header.classList.contains("minimized"),
|
|
43
148
|
pointerId: null,
|
|
@@ -54,27 +159,42 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
54
159
|
},
|
|
55
160
|
transitionHandler: null,
|
|
56
161
|
};
|
|
162
|
+
log("init:state", {
|
|
163
|
+
minimizedClass: header.classList.contains("minimized"),
|
|
164
|
+
style: { left: header.style.left, top: header.style.top, right: header.style.right, bottom: header.style.bottom },
|
|
165
|
+
rect: rectInfo(header.getBoundingClientRect())
|
|
166
|
+
});
|
|
57
167
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
168
|
+
// Restore persisted or respect DOM state on load (per path, per session)
|
|
169
|
+
const persisted = readPersisted();
|
|
170
|
+
const restoreFromStorage = !!(persisted && persisted.minimized);
|
|
171
|
+
const domIsMinimized = header.classList.contains("minimized");
|
|
172
|
+
if (restoreFromStorage || domIsMinimized) {
|
|
173
|
+
header.classList.add("minimized");
|
|
174
|
+
// Use minimized size for clamping/positioning
|
|
175
|
+
const size = measureRect((clone) => { clone.classList.add("minimized"); });
|
|
176
|
+
const fallbackLeft = Math.max(MIN_MARGIN, window.innerWidth - size.width - MIN_MARGIN);
|
|
177
|
+
const fallbackTop = Math.max(MIN_MARGIN, window.innerHeight - size.height - MIN_MARGIN);
|
|
178
|
+
const left = restoreFromStorage && Number.isFinite(persisted.left) ? persisted.left : fallbackLeft;
|
|
179
|
+
const top = restoreFromStorage && Number.isFinite(persisted.top) ? persisted.top : fallbackTop;
|
|
180
|
+
const clamped = clampPosition(left, top, size);
|
|
181
|
+
state.lastLeft = clamped.left;
|
|
182
|
+
state.lastTop = clamped.top;
|
|
183
|
+
state.hasCustomPosition = restoreFromStorage;
|
|
184
|
+
state.minimized = true;
|
|
185
|
+
// Apply immediately and once after layout settles
|
|
186
|
+
applyPosition(clamped.left, clamped.top);
|
|
187
|
+
requestAnimationFrame(() => applyPosition(clamped.left, clamped.top));
|
|
188
|
+
log("init:restore", { restoreFromStorage, domIsMinimized, measured: { width: size.width, height: size.height }, fallback: { left: fallbackLeft, top: fallbackTop }, chosen: { left, top }, clamped });
|
|
189
|
+
} else {
|
|
190
|
+
// Leave DOM/styles as rendered (expanded)
|
|
191
|
+
state.minimized = false;
|
|
192
|
+
log("init:expanded", {});
|
|
193
|
+
}
|
|
61
194
|
|
|
62
|
-
|
|
63
|
-
const rect = header.getBoundingClientRect();
|
|
64
|
-
const maxLeft = Math.max(0, window.innerWidth - rect.width);
|
|
65
|
-
const maxTop = Math.max(0, window.innerHeight - rect.height);
|
|
66
|
-
return {
|
|
67
|
-
left: Math.min(Math.max(0, left), maxLeft),
|
|
68
|
-
top: Math.min(Math.max(0, top), maxTop),
|
|
69
|
-
};
|
|
70
|
-
};
|
|
195
|
+
dispatchHeaderState(state.minimized, { phase: "init" });
|
|
71
196
|
|
|
72
|
-
|
|
73
|
-
header.style.left = `${left}px`;
|
|
74
|
-
header.style.top = `${top}px`;
|
|
75
|
-
header.style.right = "auto";
|
|
76
|
-
header.style.bottom = "auto";
|
|
77
|
-
};
|
|
197
|
+
// MIN_MARGIN is already declared above
|
|
78
198
|
|
|
79
199
|
const rememberOriginalPosition = () => {
|
|
80
200
|
state.originalPosition = {
|
|
@@ -85,33 +205,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
85
205
|
};
|
|
86
206
|
};
|
|
87
207
|
|
|
88
|
-
|
|
89
|
-
const clone = header.cloneNode(true);
|
|
90
|
-
clone.querySelectorAll("[id]").forEach((node) => node.removeAttribute("id"));
|
|
91
|
-
Object.assign(clone.style, {
|
|
92
|
-
transition: "none",
|
|
93
|
-
transform: "none",
|
|
94
|
-
position: "fixed",
|
|
95
|
-
visibility: "hidden",
|
|
96
|
-
pointerEvents: "none",
|
|
97
|
-
margin: "0",
|
|
98
|
-
left: "0",
|
|
99
|
-
top: "0",
|
|
100
|
-
right: "auto",
|
|
101
|
-
bottom: "auto",
|
|
102
|
-
width: "auto",
|
|
103
|
-
height: "auto",
|
|
104
|
-
});
|
|
105
|
-
document.body.appendChild(clone);
|
|
106
|
-
if (typeof configureClone === "function") {
|
|
107
|
-
configureClone(clone);
|
|
108
|
-
}
|
|
109
|
-
clone.style.right = "auto";
|
|
110
|
-
clone.style.bottom = "auto";
|
|
111
|
-
const rect = clone.getBoundingClientRect();
|
|
112
|
-
clone.remove();
|
|
113
|
-
return rect;
|
|
114
|
-
};
|
|
208
|
+
// measureRect declared above for early use
|
|
115
209
|
|
|
116
210
|
const stopTransition = () => {
|
|
117
211
|
if (state.transitionHandler) {
|
|
@@ -124,6 +218,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
124
218
|
header.style.transformOrigin = "";
|
|
125
219
|
header.style.opacity = "";
|
|
126
220
|
header.style.willChange = "";
|
|
221
|
+
log("transition:stop", {});
|
|
127
222
|
};
|
|
128
223
|
|
|
129
224
|
const minimize = () => {
|
|
@@ -132,6 +227,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
132
227
|
}
|
|
133
228
|
|
|
134
229
|
rememberOriginalPosition();
|
|
230
|
+
log("minimize:start", { rect: rectInfo(header.getBoundingClientRect()), original: { ...state.originalPosition } });
|
|
135
231
|
|
|
136
232
|
const firstRect = header.getBoundingClientRect();
|
|
137
233
|
const minimizedSize = measureRect((clone) => {
|
|
@@ -143,13 +239,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
143
239
|
const targetLeft = state.hasCustomPosition ? state.lastLeft : defaultLeft;
|
|
144
240
|
const targetTop = state.hasCustomPosition ? state.lastTop : defaultTop;
|
|
145
241
|
|
|
146
|
-
|
|
147
|
-
|
|
242
|
+
// Clamp final position using minimized size so it anchors bottom-right correctly
|
|
243
|
+
const clamped = clampPosition(targetLeft, targetTop, minimizedSize);
|
|
244
|
+
state.lastLeft = clamped.left;
|
|
245
|
+
state.lastTop = clamped.top;
|
|
246
|
+
log("minimize:computed", { minimizedSize, default: { left: defaultLeft, top: defaultTop }, target: { left: targetLeft, top: targetTop }, clamped });
|
|
148
247
|
|
|
149
248
|
stopTransition();
|
|
150
249
|
|
|
151
250
|
header.classList.add("minimized");
|
|
152
|
-
applyPosition(
|
|
251
|
+
applyPosition(clamped.left, clamped.top);
|
|
252
|
+
writePersisted({ minimized: true, left: clamped.left, top: clamped.top });
|
|
153
253
|
|
|
154
254
|
dispatchHeaderState(true, { phase: "start" });
|
|
155
255
|
|
|
@@ -169,6 +269,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
169
269
|
header.classList.add("transitioning");
|
|
170
270
|
header.style.transition = "transform 520ms cubic-bezier(0.22, 1, 0.36, 1)";
|
|
171
271
|
header.style.transform = "";
|
|
272
|
+
// Failsafe: clear transitioning if the event never fires
|
|
273
|
+
setTimeout(() => {
|
|
274
|
+
if (header.classList.contains("transitioning")) {
|
|
275
|
+
log("minimize:failsafe", {});
|
|
276
|
+
stopTransition();
|
|
277
|
+
}
|
|
278
|
+
}, 900);
|
|
279
|
+
// Failsafe: clear transitioning if the event never fires
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
if (header.classList.contains("transitioning")) {
|
|
282
|
+
stopTransition();
|
|
283
|
+
}
|
|
284
|
+
}, 900);
|
|
172
285
|
|
|
173
286
|
state.transitionHandler = (event) => {
|
|
174
287
|
if (event.propertyName !== "transform") {
|
|
@@ -178,18 +291,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
178
291
|
state.transitionHandler = null;
|
|
179
292
|
stopTransition();
|
|
180
293
|
state.minimized = true;
|
|
294
|
+
writePersisted({ minimized: true, left: state.lastLeft, top: state.lastTop });
|
|
181
295
|
dispatchHeaderState(true, { phase: "settled" });
|
|
296
|
+
log("minimize:done", { lastLeft: state.lastLeft, lastTop: state.lastTop });
|
|
182
297
|
};
|
|
183
298
|
|
|
184
299
|
header.addEventListener("transitionend", state.transitionHandler);
|
|
185
300
|
};
|
|
186
301
|
|
|
187
302
|
const restore = () => {
|
|
188
|
-
if (!
|
|
303
|
+
if (!header.classList.contains("minimized") || header.classList.contains("transitioning")) {
|
|
189
304
|
return;
|
|
190
305
|
}
|
|
191
306
|
|
|
192
307
|
const firstRect = header.getBoundingClientRect();
|
|
308
|
+
log("restore:start", { rect: rectInfo(firstRect), original: { ...state.originalPosition } });
|
|
193
309
|
|
|
194
310
|
stopTransition();
|
|
195
311
|
|
|
@@ -218,6 +334,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
218
334
|
|
|
219
335
|
header.style.transition = "transform 560ms cubic-bezier(0.18, 0.85, 0.4, 1)";
|
|
220
336
|
header.style.transform = "";
|
|
337
|
+
// Failsafe: clear transitioning if the event never fires
|
|
338
|
+
setTimeout(() => {
|
|
339
|
+
if (header.classList.contains("transitioning")) {
|
|
340
|
+
log("restore:failsafe", {});
|
|
341
|
+
stopTransition();
|
|
342
|
+
}
|
|
343
|
+
}, 900);
|
|
344
|
+
// Failsafe: clear transitioning if the event never fires
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
if (header.classList.contains("transitioning")) {
|
|
347
|
+
stopTransition();
|
|
348
|
+
}
|
|
349
|
+
}, 900);
|
|
221
350
|
|
|
222
351
|
state.transitionHandler = (event) => {
|
|
223
352
|
if (event.propertyName !== "transform") {
|
|
@@ -230,27 +359,36 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
230
359
|
state.hasCustomPosition = false;
|
|
231
360
|
state.lastLeft = parseFloat(header.style.left) || 0;
|
|
232
361
|
state.lastTop = parseFloat(header.style.top) || 0;
|
|
362
|
+
writePersisted({ minimized: false, left: state.lastLeft, top: state.lastTop });
|
|
233
363
|
dispatchHeaderState(false, { phase: "settled" });
|
|
364
|
+
log("restore:done", { left: state.lastLeft, top: state.lastTop });
|
|
234
365
|
};
|
|
235
366
|
|
|
236
367
|
header.addEventListener("transitionend", state.transitionHandler);
|
|
237
368
|
};
|
|
238
369
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
370
|
+
if (minimizeButton) {
|
|
371
|
+
minimizeButton.addEventListener("click", (event) => {
|
|
372
|
+
log("click:minimize", {});
|
|
373
|
+
event.preventDefault();
|
|
374
|
+
minimize();
|
|
375
|
+
});
|
|
376
|
+
}
|
|
243
377
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
378
|
+
if (homeLink) {
|
|
379
|
+
homeLink.addEventListener("click", (event) => {
|
|
380
|
+
const minimizedNow = header.classList.contains("minimized");
|
|
381
|
+
log("click:home", { minimizedNow });
|
|
382
|
+
if (!minimizedNow) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
event.preventDefault();
|
|
386
|
+
restore();
|
|
387
|
+
});
|
|
388
|
+
}
|
|
251
389
|
|
|
252
390
|
const onPointerDown = (event) => {
|
|
253
|
-
if (!
|
|
391
|
+
if (!header.classList.contains("minimized") || header.classList.contains("transitioning")) {
|
|
254
392
|
return;
|
|
255
393
|
}
|
|
256
394
|
state.pointerId = event.pointerId;
|
|
@@ -263,11 +401,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
263
401
|
} catch (error) {}
|
|
264
402
|
}
|
|
265
403
|
dragHandle.classList.add("dragging");
|
|
404
|
+
log("drag:start", { pointerId: state.pointerId, rect: rectInfo(rect), offsetX: Math.round(state.offsetX), offsetY: Math.round(state.offsetY) });
|
|
266
405
|
event.preventDefault();
|
|
267
406
|
};
|
|
268
407
|
|
|
408
|
+
let __lastPointerLog = 0;
|
|
269
409
|
const onPointerMove = (event) => {
|
|
270
|
-
if (!
|
|
410
|
+
if (!header.classList.contains("minimized") || state.pointerId !== event.pointerId) {
|
|
271
411
|
return;
|
|
272
412
|
}
|
|
273
413
|
const left = event.clientX - state.offsetX;
|
|
@@ -277,6 +417,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
277
417
|
state.lastTop = clamped.top;
|
|
278
418
|
state.hasCustomPosition = true;
|
|
279
419
|
applyPosition(clamped.left, clamped.top);
|
|
420
|
+
writePersisted({ minimized: true, left: clamped.left, top: clamped.top });
|
|
421
|
+
const t = Date.now();
|
|
422
|
+
if (t - __lastPointerLog > 150) {
|
|
423
|
+
__lastPointerLog = t;
|
|
424
|
+
log("drag:move", { left, top, clamped });
|
|
425
|
+
}
|
|
280
426
|
};
|
|
281
427
|
|
|
282
428
|
const onPointerEnd = (event) => {
|
|
@@ -289,6 +435,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
289
435
|
} catch (error) {}
|
|
290
436
|
}
|
|
291
437
|
dragHandle.classList.remove("dragging");
|
|
438
|
+
log("drag:end", { pointerId: event.pointerId, lastLeft: state.lastLeft, lastTop: state.lastTop });
|
|
292
439
|
state.pointerId = null;
|
|
293
440
|
};
|
|
294
441
|
|
|
@@ -296,14 +443,30 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
296
443
|
dragHandle.addEventListener("pointermove", onPointerMove);
|
|
297
444
|
dragHandle.addEventListener("pointerup", onPointerEnd);
|
|
298
445
|
dragHandle.addEventListener("pointercancel", onPointerEnd);
|
|
446
|
+
// Fallback: allow dragging by grabbing empty areas of the minimized header
|
|
447
|
+
const isInteractive = (el) => !!el.closest("a, button, [role='button'], input, select, textarea");
|
|
448
|
+
header.addEventListener("pointerdown", (event) => {
|
|
449
|
+
if (!header.classList.contains("minimized") || isInteractive(event.target) || event.target.closest(".header-drag-handle")) {
|
|
450
|
+
// Interactive controls and the dedicated drag handle use their own handlers
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
log("drag:fallback:pointerdown", { target: event.target && (event.target.id || event.target.className || event.target.nodeName) });
|
|
454
|
+
onPointerDown(event);
|
|
455
|
+
});
|
|
456
|
+
header.addEventListener("pointermove", onPointerMove);
|
|
457
|
+
header.addEventListener("pointerup", onPointerEnd);
|
|
458
|
+
header.addEventListener("pointercancel", onPointerEnd);
|
|
299
459
|
|
|
300
460
|
window.addEventListener("resize", () => {
|
|
301
|
-
if (!
|
|
461
|
+
if (!header.classList.contains("minimized") || header.classList.contains("transitioning")) {
|
|
302
462
|
return;
|
|
303
463
|
}
|
|
464
|
+
const before = { left: state.lastLeft, top: state.lastTop };
|
|
304
465
|
const { left, top } = clampPosition(state.lastLeft, state.lastTop);
|
|
305
466
|
state.lastLeft = left;
|
|
306
467
|
state.lastTop = top;
|
|
307
468
|
applyPosition(left, top);
|
|
469
|
+
writePersisted({ minimized: state.minimized, left, top });
|
|
470
|
+
log("resize:clamp", { before, after: { left, top } });
|
|
308
471
|
});
|
|
309
472
|
});
|
package/server/public/style.css
CHANGED
|
@@ -2383,6 +2383,8 @@ body.dark .mode-selector .btn2.selected {
|
|
|
2383
2383
|
|
|
2384
2384
|
|
|
2385
2385
|
@media only screen and (max-width: 768px) {
|
|
2386
|
+
/* Hide QR block on small screens */
|
|
2387
|
+
aside .qr { display: none !important; }
|
|
2386
2388
|
body {
|
|
2387
2389
|
display: flex;
|
|
2388
2390
|
align-items: stretch;
|
|
@@ -2455,6 +2457,14 @@ body.dark .mode-selector .btn2.selected {
|
|
|
2455
2457
|
overflow: auto;
|
|
2456
2458
|
flex-wrap: nowrap;
|
|
2457
2459
|
}
|
|
2460
|
+
/* Keep minimized header horizontal on small screens */
|
|
2461
|
+
header.navheader.minimized,
|
|
2462
|
+
header.navheader.minimized h1 {
|
|
2463
|
+
display: inline-flex;
|
|
2464
|
+
flex-direction: row;
|
|
2465
|
+
}
|
|
2466
|
+
/* Avoid stretching buttons to full width inside minimized pill */
|
|
2467
|
+
header.navheader.minimized h1 .btn2 { width: auto; }
|
|
2458
2468
|
.mode-selector {
|
|
2459
2469
|
flex-direction: column;
|
|
2460
2470
|
padding: 0;
|
|
@@ -2539,8 +2549,18 @@ header.navheader.minimized h1 {
|
|
|
2539
2549
|
padding: 0;
|
|
2540
2550
|
height: auto;
|
|
2541
2551
|
overflow: visible;
|
|
2542
|
-
|
|
2543
|
-
|
|
2552
|
+
flex-wrap: nowrap;
|
|
2553
|
+
justify-content: flex-start;
|
|
2554
|
+
}
|
|
2555
|
+
/* Ensure item order inside minimized pill: home, refresh, dragger */
|
|
2556
|
+
header.navheader.minimized h1 .home { order: 0; }
|
|
2557
|
+
header.navheader.minimized h1 #refresh-page { order: 1; }
|
|
2558
|
+
header.navheader.minimized h1 .header-drag-handle { order: 2; }
|
|
2559
|
+
/* Prevent items from stretching */
|
|
2560
|
+
header.navheader.minimized h1 > .home,
|
|
2561
|
+
header.navheader.minimized h1 > #refresh-page,
|
|
2562
|
+
header.navheader.minimized h1 > .header-drag-handle { flex: 0 0 auto; }
|
|
2563
|
+
header.navheader.minimized h1 > *:not(.home):not(.header-drag-handle):not(#refresh-page) {
|
|
2544
2564
|
display: none !important;
|
|
2545
2565
|
}
|
|
2546
2566
|
header.navheader .header-drag-handle {
|
|
@@ -492,6 +492,9 @@ const ensureTabAccessories = aggregateDebounce(() => {
|
|
|
492
492
|
message,
|
|
493
493
|
timeout: 60,
|
|
494
494
|
sound: true,
|
|
495
|
+
// Target this notification to this browser/device only
|
|
496
|
+
audience: 'device',
|
|
497
|
+
device_id: (typeof window !== 'undefined' && typeof window.PinokioGetDeviceId === 'function') ? window.PinokioGetDeviceId() : undefined,
|
|
495
498
|
};
|
|
496
499
|
if (image) {
|
|
497
500
|
payload.image = image;
|
package/server/socket.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const querystring = require("querystring");
|
|
2
2
|
const WebSocket = require('ws');
|
|
3
3
|
const path = require('path')
|
|
4
|
+
const os = require('os')
|
|
4
5
|
const Util = require("../kernel/util")
|
|
5
6
|
const Environment = require("../kernel/environment")
|
|
6
7
|
const NOTIFICATION_CHANNEL = 'kernel.notifications'
|
|
@@ -15,6 +16,20 @@ class Socket {
|
|
|
15
16
|
this.server = parent.server
|
|
16
17
|
// this.kernel = parent.kernel
|
|
17
18
|
const wss = new WebSocket.Server({ server: this.parent.server })
|
|
19
|
+
this.localDeviceIds = new Set()
|
|
20
|
+
this.localAddresses = new Set()
|
|
21
|
+
try {
|
|
22
|
+
const ifaces = os.networkInterfaces() || {}
|
|
23
|
+
Object.values(ifaces).forEach((arr) => {
|
|
24
|
+
(arr || []).forEach((info) => {
|
|
25
|
+
if (info && info.address) {
|
|
26
|
+
this.localAddresses.add(info.address)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
this.localAddresses.add('127.0.0.1')
|
|
31
|
+
this.localAddresses.add('::1')
|
|
32
|
+
} catch (_) {}
|
|
18
33
|
this.subscriptions = new Map(); // Initialize a Map to store the WebSocket connections interested in each event
|
|
19
34
|
this.notificationChannel = NOTIFICATION_CHANNEL
|
|
20
35
|
this.notificationBridgeDispose = null
|
|
@@ -36,6 +51,12 @@ class Socket {
|
|
|
36
51
|
this.subscriptions.delete(eventName);
|
|
37
52
|
}
|
|
38
53
|
});
|
|
54
|
+
// Cleanup device tracking
|
|
55
|
+
try {
|
|
56
|
+
if (ws._isLocalClient && ws._deviceId) {
|
|
57
|
+
this.localDeviceIds.delete(ws._deviceId)
|
|
58
|
+
}
|
|
59
|
+
} catch (_) {}
|
|
39
60
|
this.checkNotificationBridge();
|
|
40
61
|
});
|
|
41
62
|
ws.on('message', async (message, isBinary) => {
|
|
@@ -171,6 +192,25 @@ class Socket {
|
|
|
171
192
|
this.parent.kernel.api.process(req)
|
|
172
193
|
}
|
|
173
194
|
} else {
|
|
195
|
+
if (req.method === this.notificationChannel) {
|
|
196
|
+
if (typeof req.device_id === 'string' && req.device_id.trim()) {
|
|
197
|
+
ws._deviceId = req.device_id.trim()
|
|
198
|
+
}
|
|
199
|
+
// Mark local client sockets by IP matching any local address
|
|
200
|
+
try {
|
|
201
|
+
const ip = ws._ip || ''
|
|
202
|
+
const isLocal = (addr) => {
|
|
203
|
+
if (!addr || typeof addr !== 'string') return false
|
|
204
|
+
if (this.localAddresses.has(addr)) return true
|
|
205
|
+
const v = addr.trim().toLowerCase()
|
|
206
|
+
return v.startsWith('::ffff:127.') || v.startsWith('127.')
|
|
207
|
+
}
|
|
208
|
+
ws._isLocalClient = isLocal(ip)
|
|
209
|
+
if (ws._isLocalClient && ws._deviceId) {
|
|
210
|
+
this.localDeviceIds.add(ws._deviceId)
|
|
211
|
+
}
|
|
212
|
+
} catch (_) {}
|
|
213
|
+
}
|
|
174
214
|
this.subscribe(ws, req.method)
|
|
175
215
|
if (req.mode !== "listen") {
|
|
176
216
|
this.parent.kernel.api.process(req)
|
|
@@ -350,11 +390,38 @@ class Socket {
|
|
|
350
390
|
data: payload,
|
|
351
391
|
}
|
|
352
392
|
const frame = JSON.stringify(envelope)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
393
|
+
const targetId = (payload && typeof payload.device_id === 'string' && payload.device_id.trim()) ? payload.device_id.trim() : null
|
|
394
|
+
const audience = (payload && typeof payload.audience === 'string' && payload.audience.trim()) ? payload.audience.trim() : null
|
|
395
|
+
if (audience === 'device' && targetId) {
|
|
396
|
+
let delivered = false
|
|
397
|
+
subscribers.forEach((subscriber) => {
|
|
398
|
+
if (subscriber.readyState !== WebSocket.OPEN) {
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
if (subscriber._deviceId && subscriber._deviceId === targetId) {
|
|
402
|
+
try { subscriber.send(frame); delivered = true } catch (_) {}
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
if (!delivered) {
|
|
406
|
+
// Fallback: broadcast if no matching device subscriber is available
|
|
407
|
+
subscribers.forEach((subscriber) => {
|
|
408
|
+
if (subscriber.readyState === WebSocket.OPEN) {
|
|
409
|
+
try { subscriber.send(frame) } catch (_) {}
|
|
410
|
+
}
|
|
411
|
+
})
|
|
356
412
|
}
|
|
357
|
-
}
|
|
413
|
+
} else {
|
|
414
|
+
subscribers.forEach((subscriber) => {
|
|
415
|
+
if (subscriber.readyState === WebSocket.OPEN) {
|
|
416
|
+
try { subscriber.send(frame) } catch (_) {}
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
isLocalDevice(deviceId) {
|
|
423
|
+
if (!deviceId || typeof deviceId !== 'string') return false
|
|
424
|
+
return this.localDeviceIds.has(deviceId)
|
|
358
425
|
}
|
|
359
426
|
|
|
360
427
|
ensureNotificationBridge() {
|