pinokiod 3.170.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 +249 -26
- package/server/public/common.js +244 -0
- package/server/public/files-app/app.css +73 -1
- package/server/public/files-app/app.js +255 -2
- package/server/public/layout.js +115 -1
- package/server/public/nav.js +227 -64
- package/server/public/style.css +27 -3
- package/server/public/tab-idle-notifier.js +3 -0
- package/server/routes/files.js +96 -0
- package/server/socket.js +71 -4
- package/server/views/app.ejs +603 -53
- package/server/views/connect.ejs +9 -0
- package/server/views/file_browser.ejs +9 -2
- package/server/views/index.ejs +11 -2
- package/server/views/init/index.ejs +9 -2
- package/server/views/layout.ejs +7 -5
- package/server/views/net.ejs +9 -0
- package/server/views/network.ejs +9 -0
- package/server/views/review.ejs +4 -3
- 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/layout.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
const HOST_ORIGIN = window.location.origin;
|
|
24
24
|
const STORAGE_PREFIX = 'pinokio:layout:';
|
|
25
25
|
const MIN_PANEL_SIZE = 120;
|
|
26
|
-
const GUTTER_SIZE =
|
|
26
|
+
const GUTTER_SIZE = 6;
|
|
27
27
|
|
|
28
28
|
const state = {
|
|
29
29
|
sessionId: typeof parsedConfig.sessionId === 'string' && parsedConfig.sessionId.trim() ? parsedConfig.sessionId.trim() : null,
|
|
@@ -790,4 +790,118 @@
|
|
|
790
790
|
};
|
|
791
791
|
|
|
792
792
|
window.PinokioLayout = api;
|
|
793
|
+
// Mobile "Tap to connect" curtain is centralized in common.js to avoid duplicates
|
|
794
|
+
|
|
795
|
+
// Top-level notification listener (indicator + optional chime) for mobile
|
|
796
|
+
(function initTopLevelNotificationListener() {
|
|
797
|
+
try { if (window.top && window.top !== window) return; } catch (_) { return; }
|
|
798
|
+
if (window.__pinokioTopNotifyListener) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
window.__pinokioTopNotifyListener = true;
|
|
802
|
+
|
|
803
|
+
const ensureIndicator = (() => {
|
|
804
|
+
let el = null;
|
|
805
|
+
let styleInjected = false;
|
|
806
|
+
return () => {
|
|
807
|
+
if (!styleInjected) {
|
|
808
|
+
const style = document.createElement('style');
|
|
809
|
+
style.textContent = `
|
|
810
|
+
.pinokio-notify-indicator{position:fixed;top:12px;right:12px;z-index:2147483647;display:none;align-items:center;gap:8px;padding:8px 10px;border-radius:999px;background:rgba(15,23,42,0.92);color:#fff;font:600 12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;box-shadow:0 10px 30px rgba(0,0,0,0.35)}
|
|
811
|
+
.pinokio-notify-indicator .bell{font-size:14px}
|
|
812
|
+
.pinokio-notify-indicator.show{display:inline-flex;animation:pinokioNotifyPop 160ms ease-out, pinokioNotifyFade 1600ms ease-in 700ms forwards}
|
|
813
|
+
@keyframes pinokioNotifyPop{from{transform:translateY(-6px) scale(.98);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}
|
|
814
|
+
@keyframes pinokioNotifyFade{to{opacity:0;transform:translateY(-4px)}}
|
|
815
|
+
@media (max-width: 768px){.pinokio-notify-indicator{top:10px;right:10px;padding:7px 9px;font-size:12px}}
|
|
816
|
+
`;
|
|
817
|
+
document.head.appendChild(style);
|
|
818
|
+
styleInjected = true;
|
|
819
|
+
}
|
|
820
|
+
if (!el) {
|
|
821
|
+
el = document.createElement('div');
|
|
822
|
+
el.className = 'pinokio-notify-indicator';
|
|
823
|
+
const icon = document.createElement('span');
|
|
824
|
+
icon.className = 'bell';
|
|
825
|
+
icon.textContent = '🔔';
|
|
826
|
+
const text = document.createElement('span');
|
|
827
|
+
text.className = 'text';
|
|
828
|
+
text.textContent = 'Notification received';
|
|
829
|
+
el.appendChild(icon);
|
|
830
|
+
el.appendChild(text);
|
|
831
|
+
document.body.appendChild(el);
|
|
832
|
+
}
|
|
833
|
+
return el;
|
|
834
|
+
};
|
|
835
|
+
})();
|
|
836
|
+
|
|
837
|
+
const flashIndicator = (message) => {
|
|
838
|
+
const node = ensureIndicator();
|
|
839
|
+
const text = node.querySelector('.text');
|
|
840
|
+
if (text) {
|
|
841
|
+
const msg = (message && typeof message === 'string' && message.trim()) ? message.trim() : 'Notification received';
|
|
842
|
+
text.textContent = msg.length > 80 ? (msg.slice(0,77) + '…') : msg;
|
|
843
|
+
}
|
|
844
|
+
node.classList.remove('show');
|
|
845
|
+
void node.offsetWidth;
|
|
846
|
+
node.classList.add('show');
|
|
847
|
+
setTimeout(() => node.classList.remove('show'), 2400);
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
const tryPlay = (url) => {
|
|
851
|
+
try {
|
|
852
|
+
const src = (typeof url === 'string' && url) ? url : '/chime.mp3';
|
|
853
|
+
let a = window.__pinokioChimeAudio;
|
|
854
|
+
if (!a) {
|
|
855
|
+
a = new Audio(src);
|
|
856
|
+
a.preload = 'auto';
|
|
857
|
+
a.loop = false;
|
|
858
|
+
a.muted = false;
|
|
859
|
+
window.__pinokioChimeAudio = a;
|
|
860
|
+
} else {
|
|
861
|
+
try { if (a.src && !a.src.endsWith(src)) a.src = src; } catch (_) {}
|
|
862
|
+
}
|
|
863
|
+
try { a.currentTime = 0; } catch (_) {}
|
|
864
|
+
const p = a.play();
|
|
865
|
+
if (p && typeof p.catch === 'function') { p.catch(() => {}); }
|
|
866
|
+
if (typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function') {
|
|
867
|
+
try { navigator.vibrate(80); } catch (_) {}
|
|
868
|
+
}
|
|
869
|
+
} catch (_) {}
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
const listen = () => {
|
|
873
|
+
const SocketCtor = typeof window.Socket === 'function' ? window.Socket : (typeof Socket === 'function' ? Socket : null);
|
|
874
|
+
if (!SocketCtor || typeof WebSocket === 'undefined') {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const socket = new SocketCtor();
|
|
878
|
+
try {
|
|
879
|
+
socket.run({ method: 'kernel.notifications', mode: 'listen', device_id: (typeof window.PinokioGetDeviceId === 'function') ? window.PinokioGetDeviceId() : undefined }, (packet) => {
|
|
880
|
+
if (!packet || packet.id !== 'kernel.notifications' || packet.type !== 'notification') {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const payload = packet.data || {};
|
|
884
|
+
// If targeted to a specific device, ignore only when our id exists and mismatches
|
|
885
|
+
try {
|
|
886
|
+
const targetId = (typeof payload.device_id === 'string' && payload.device_id.trim()) ? payload.device_id.trim() : null;
|
|
887
|
+
if (targetId) {
|
|
888
|
+
const myId = (typeof window.PinokioGetDeviceId === 'function') ? window.PinokioGetDeviceId() : null;
|
|
889
|
+
if (myId && myId !== targetId) return;
|
|
890
|
+
}
|
|
891
|
+
} catch (_) {}
|
|
892
|
+
flashIndicator(payload.message);
|
|
893
|
+
tryPlay(payload.sound);
|
|
894
|
+
}).then(() => {
|
|
895
|
+
// socket closed; ignore
|
|
896
|
+
}).catch(() => {});
|
|
897
|
+
window.__pinokioTopNotifySocket = socket;
|
|
898
|
+
} catch (_) {}
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
if (document.readyState === 'loading') {
|
|
902
|
+
document.addEventListener('DOMContentLoaded', listen, { once: true });
|
|
903
|
+
} else {
|
|
904
|
+
listen();
|
|
905
|
+
}
|
|
906
|
+
})();
|
|
793
907
|
})();
|
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
|
@@ -821,6 +821,10 @@ body {
|
|
|
821
821
|
color: var(--light-color);
|
|
822
822
|
position: relative;
|
|
823
823
|
}
|
|
824
|
+
/* Reserve scrollbar space to prevent header layout shift */
|
|
825
|
+
html {
|
|
826
|
+
scrollbar-gutter: stable both-edges;
|
|
827
|
+
}
|
|
824
828
|
body.dark {
|
|
825
829
|
color: var(--dark-color);
|
|
826
830
|
background: var(--dark-bg);
|
|
@@ -2371,7 +2375,7 @@ body.dark .mode-selector .btn2.selected {
|
|
|
2371
2375
|
justify-content: center;
|
|
2372
2376
|
padding: 5px 10px;
|
|
2373
2377
|
box-sizing: border-box;
|
|
2374
|
-
width:
|
|
2378
|
+
width: 70px;
|
|
2375
2379
|
gap: 5px;
|
|
2376
2380
|
border-radius: 5px;
|
|
2377
2381
|
}
|
|
@@ -2379,6 +2383,8 @@ body.dark .mode-selector .btn2.selected {
|
|
|
2379
2383
|
|
|
2380
2384
|
|
|
2381
2385
|
@media only screen and (max-width: 768px) {
|
|
2386
|
+
/* Hide QR block on small screens */
|
|
2387
|
+
aside .qr { display: none !important; }
|
|
2382
2388
|
body {
|
|
2383
2389
|
display: flex;
|
|
2384
2390
|
align-items: stretch;
|
|
@@ -2451,6 +2457,14 @@ body.dark .mode-selector .btn2.selected {
|
|
|
2451
2457
|
overflow: auto;
|
|
2452
2458
|
flex-wrap: nowrap;
|
|
2453
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; }
|
|
2454
2468
|
.mode-selector {
|
|
2455
2469
|
flex-direction: column;
|
|
2456
2470
|
padding: 0;
|
|
@@ -2535,8 +2549,18 @@ header.navheader.minimized h1 {
|
|
|
2535
2549
|
padding: 0;
|
|
2536
2550
|
height: auto;
|
|
2537
2551
|
overflow: visible;
|
|
2538
|
-
|
|
2539
|
-
|
|
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) {
|
|
2540
2564
|
display: none !important;
|
|
2541
2565
|
}
|
|
2542
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;
|