pinokiod 3.180.0 → 3.182.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 +28 -4
- package/package.json +1 -1
- package/server/index.js +237 -35
- package/server/public/common.js +677 -240
- package/server/public/files-app/app.css +64 -0
- package/server/public/files-app/app.js +87 -0
- package/server/public/install.js +8 -1
- package/server/public/layout.js +124 -0
- package/server/public/nav.js +227 -64
- package/server/public/sound/beep.mp3 +0 -0
- package/server/public/sound/bell.mp3 +0 -0
- package/server/public/sound/bright-ring.mp3 +0 -0
- package/server/public/sound/clap.mp3 +0 -0
- package/server/public/sound/deep-ring.mp3 +0 -0
- package/server/public/sound/gasp.mp3 +0 -0
- package/server/public/sound/hehe.mp3 +0 -0
- package/server/public/sound/levelup.mp3 +0 -0
- package/server/public/sound/light-pop.mp3 +0 -0
- package/server/public/sound/light-ring.mp3 +0 -0
- package/server/public/sound/meow.mp3 +0 -0
- package/server/public/sound/piano.mp3 +0 -0
- package/server/public/sound/pop.mp3 +0 -0
- package/server/public/sound/uhoh.mp3 +0 -0
- package/server/public/sound/whistle.mp3 +0 -0
- package/server/public/style.css +195 -4
- package/server/public/tab-idle-notifier.js +700 -4
- package/server/public/terminal-settings.js +1131 -0
- package/server/public/urldropdown.css +28 -1
- package/server/socket.js +71 -4
- package/server/views/{terminals.ejs → agents.ejs} +108 -32
- package/server/views/app.ejs +321 -104
- package/server/views/bootstrap.ejs +8 -0
- package/server/views/connect.ejs +10 -1
- package/server/views/d.ejs +172 -18
- package/server/views/editor.ejs +8 -0
- package/server/views/file_browser.ejs +4 -0
- package/server/views/index.ejs +10 -1
- package/server/views/init/index.ejs +18 -3
- package/server/views/install.ejs +8 -0
- package/server/views/layout.ejs +2 -0
- package/server/views/net.ejs +10 -1
- package/server/views/network.ejs +10 -1
- package/server/views/pro.ejs +8 -0
- package/server/views/prototype/index.ejs +8 -0
- package/server/views/screenshots.ejs +10 -2
- package/server/views/settings.ejs +10 -2
- package/server/views/shell.ejs +8 -0
- package/server/views/terminal.ejs +8 -0
- package/server/views/tools.ejs +10 -2
|
@@ -19,6 +19,20 @@
|
|
|
19
19
|
const TAB_DETAILS_CLASS = 'tab-details';
|
|
20
20
|
const PREF_STORAGE_KEY = 'pinokio:idle-prefs';
|
|
21
21
|
const notifyPreferences = new Map();
|
|
22
|
+
const SOUND_PREF_STORAGE_KEY = 'pinokio:idle-sound';
|
|
23
|
+
const SOUND_DEFAULT_CHOICE = '__default__';
|
|
24
|
+
const SOUND_SILENT_CHOICE = '__silent__';
|
|
25
|
+
const SOUND_LIST_ENDPOINT = '/pinokio/notification-sounds';
|
|
26
|
+
const DEFAULT_SOUND_URL = '/chime.mp3';
|
|
27
|
+
let globalSoundPreference = { choice: SOUND_DEFAULT_CHOICE };
|
|
28
|
+
let soundOptionsCache = null;
|
|
29
|
+
let soundOptionsPromise = null;
|
|
30
|
+
let previewAudio = null;
|
|
31
|
+
let soundMenuNode = null;
|
|
32
|
+
let soundMenuContent = null;
|
|
33
|
+
let openMenuContext = null;
|
|
34
|
+
const MENU_KEY_TOGGLE = 'toggle';
|
|
35
|
+
let dismissOverlay = null;
|
|
22
36
|
|
|
23
37
|
const hydratePreferences = () => {
|
|
24
38
|
try {
|
|
@@ -54,6 +68,566 @@
|
|
|
54
68
|
}
|
|
55
69
|
};
|
|
56
70
|
|
|
71
|
+
const normaliseSoundAssetPath = (value) => {
|
|
72
|
+
if (typeof value !== 'string') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const trimmed = value.trim();
|
|
76
|
+
if (!trimmed) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const withLeading = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
80
|
+
if (!withLeading.startsWith('/sound/')) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const decoded = decodeURIComponent(withLeading);
|
|
85
|
+
if (decoded.includes('..')) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
} catch (_) {
|
|
89
|
+
if (withLeading.includes('..')) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return withLeading;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const normaliseSoundChoice = (value) => {
|
|
97
|
+
if (value === SOUND_SILENT_CHOICE) {
|
|
98
|
+
return SOUND_SILENT_CHOICE;
|
|
99
|
+
}
|
|
100
|
+
if (value === SOUND_DEFAULT_CHOICE) {
|
|
101
|
+
return SOUND_DEFAULT_CHOICE;
|
|
102
|
+
}
|
|
103
|
+
const asset = normaliseSoundAssetPath(value);
|
|
104
|
+
if (asset) {
|
|
105
|
+
return asset;
|
|
106
|
+
}
|
|
107
|
+
return SOUND_DEFAULT_CHOICE;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const hydrateSoundPreference = () => {
|
|
111
|
+
globalSoundPreference = { choice: SOUND_DEFAULT_CHOICE };
|
|
112
|
+
try {
|
|
113
|
+
const raw = localStorage.getItem(SOUND_PREF_STORAGE_KEY);
|
|
114
|
+
if (!raw) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
if (parsed && typeof parsed === 'object' && parsed !== null) {
|
|
119
|
+
globalSoundPreference.choice = normaliseSoundChoice(parsed.choice);
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
log('Failed to hydrate sound preference', error);
|
|
123
|
+
globalSoundPreference = { choice: SOUND_DEFAULT_CHOICE };
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const persistSoundPreference = () => {
|
|
128
|
+
try {
|
|
129
|
+
const choice = globalSoundPreference?.choice;
|
|
130
|
+
if (!choice || choice === SOUND_DEFAULT_CHOICE) {
|
|
131
|
+
localStorage.removeItem(SOUND_PREF_STORAGE_KEY);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
localStorage.setItem(SOUND_PREF_STORAGE_KEY, JSON.stringify({ choice }));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
log('Failed to persist sound preference', error);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const escapeHtml = (value) => {
|
|
141
|
+
if (typeof value !== 'string') {
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
return value
|
|
145
|
+
.replace(/&/g, '&')
|
|
146
|
+
.replace(/</g, '<')
|
|
147
|
+
.replace(/>/g, '>')
|
|
148
|
+
.replace(/"/g, '"')
|
|
149
|
+
.replace(/'/g, ''');
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const resolveNotificationSound = () => {
|
|
153
|
+
const choice = globalSoundPreference?.choice;
|
|
154
|
+
if (choice === SOUND_SILENT_CHOICE) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const asset = normaliseSoundAssetPath(choice);
|
|
158
|
+
if (asset) {
|
|
159
|
+
return asset;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const baseSoundOptions = () => ([
|
|
165
|
+
{ value: SOUND_DEFAULT_CHOICE, label: 'Default Chime', preview: DEFAULT_SOUND_URL },
|
|
166
|
+
{ value: SOUND_SILENT_CHOICE, label: 'Silent', preview: null },
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const loadSoundOptions = () => {
|
|
170
|
+
if (soundOptionsCache) {
|
|
171
|
+
return Promise.resolve(soundOptionsCache.map((option) => ({ ...option })));
|
|
172
|
+
}
|
|
173
|
+
if (!soundOptionsPromise) {
|
|
174
|
+
soundOptionsPromise = fetch(SOUND_LIST_ENDPOINT, { credentials: 'include' })
|
|
175
|
+
.then((response) => {
|
|
176
|
+
if (!response || !response.ok) {
|
|
177
|
+
throw new Error(`Failed to load sound list (${response ? response.status : 'no response'})`);
|
|
178
|
+
}
|
|
179
|
+
return response.json();
|
|
180
|
+
})
|
|
181
|
+
.then((data) => {
|
|
182
|
+
const dynamic = Array.isArray(data?.sounds) ? data.sounds : [];
|
|
183
|
+
const mapped = dynamic
|
|
184
|
+
.map((item) => {
|
|
185
|
+
if (!item || typeof item.url !== 'string') {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const asset = normaliseSoundAssetPath(item.url || item.id || item.filename);
|
|
189
|
+
if (!asset) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const label = typeof item.label === 'string' && item.label.trim()
|
|
193
|
+
? item.label.trim()
|
|
194
|
+
: (typeof item.filename === 'string' && item.filename.trim()
|
|
195
|
+
? item.filename.trim()
|
|
196
|
+
: asset.replace(/^\/+/, ''));
|
|
197
|
+
return {
|
|
198
|
+
value: asset,
|
|
199
|
+
label,
|
|
200
|
+
preview: asset,
|
|
201
|
+
};
|
|
202
|
+
})
|
|
203
|
+
.filter((option) => option && option.value && option.label);
|
|
204
|
+
|
|
205
|
+
const deduped = new Map();
|
|
206
|
+
baseSoundOptions().forEach((option) => {
|
|
207
|
+
deduped.set(option.value, option);
|
|
208
|
+
});
|
|
209
|
+
mapped.forEach((option) => {
|
|
210
|
+
deduped.set(option.value, option);
|
|
211
|
+
});
|
|
212
|
+
soundOptionsCache = Array.from(deduped.values());
|
|
213
|
+
return soundOptionsCache.map((option) => ({ ...option }));
|
|
214
|
+
})
|
|
215
|
+
.catch((error) => {
|
|
216
|
+
log('Failed to load notification sound list', error);
|
|
217
|
+
return baseSoundOptions().map((option) => ({ ...option }));
|
|
218
|
+
})
|
|
219
|
+
.finally(() => {
|
|
220
|
+
soundOptionsPromise = null;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return soundOptionsPromise.then((options) => options.map((option) => ({ ...option })));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const getPreviewUrlForChoice = (choice) => {
|
|
227
|
+
if (choice === SOUND_SILENT_CHOICE) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const asset = normaliseSoundAssetPath(choice);
|
|
231
|
+
if (asset) {
|
|
232
|
+
return asset;
|
|
233
|
+
}
|
|
234
|
+
return DEFAULT_SOUND_URL;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const playSoundPreview = (choice) => {
|
|
238
|
+
const url = getPreviewUrlForChoice(choice);
|
|
239
|
+
if (!url) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
if (!previewAudio) {
|
|
244
|
+
previewAudio = new Audio();
|
|
245
|
+
previewAudio.preload = 'auto';
|
|
246
|
+
previewAudio.loop = false;
|
|
247
|
+
previewAudio.muted = false;
|
|
248
|
+
}
|
|
249
|
+
const resolved = new URL(url, window.location.origin).toString();
|
|
250
|
+
if (previewAudio.src !== resolved) {
|
|
251
|
+
previewAudio.src = resolved;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
previewAudio.currentTime = 0;
|
|
255
|
+
} catch (_) {}
|
|
256
|
+
const result = previewAudio.play();
|
|
257
|
+
if (result && typeof result.catch === 'function') {
|
|
258
|
+
result.catch(() => {});
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
log('Failed to play sound preview', error);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const getMenuKeyForSoundValue = (value) => `sound:${value}`;
|
|
266
|
+
|
|
267
|
+
const getMenuItems = () => {
|
|
268
|
+
if (!soundMenuContent) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
return Array.from(soundMenuContent.querySelectorAll('[data-menu-item="true"]'));
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const positionSoundMenu = (menu, anchor) => {
|
|
275
|
+
if (!menu || !anchor || typeof anchor.getBoundingClientRect !== 'function') {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const rect = anchor.getBoundingClientRect();
|
|
279
|
+
const scrollX = window.scrollX || window.pageXOffset || 0;
|
|
280
|
+
const scrollY = window.scrollY || window.pageYOffset || 0;
|
|
281
|
+
const top = rect.bottom + scrollY + 6;
|
|
282
|
+
let left = rect.left + scrollX;
|
|
283
|
+
const menuWidth = menu.offsetWidth || 0;
|
|
284
|
+
const viewportRight = scrollX + window.innerWidth;
|
|
285
|
+
if (left + menuWidth > viewportRight - 12) {
|
|
286
|
+
left = Math.max(scrollX + 12, viewportRight - menuWidth - 12);
|
|
287
|
+
}
|
|
288
|
+
if (left < scrollX + 12) {
|
|
289
|
+
left = scrollX + 12;
|
|
290
|
+
}
|
|
291
|
+
menu.style.top = `${Math.round(top)}px`;
|
|
292
|
+
menu.style.left = `${Math.round(left)}px`;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const updateMenuPosition = () => {
|
|
296
|
+
if (!openMenuContext || !soundMenuNode) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
positionSoundMenu(soundMenuNode, openMenuContext.toggle);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const closeSoundMenu = (focusAnchor = false) => {
|
|
303
|
+
if (!openMenuContext) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const { toggle } = openMenuContext;
|
|
307
|
+
if (soundMenuNode) {
|
|
308
|
+
soundMenuNode.classList.remove('is-open');
|
|
309
|
+
soundMenuNode.setAttribute('aria-hidden', 'true');
|
|
310
|
+
soundMenuNode.style.top = '-9999px';
|
|
311
|
+
soundMenuNode.style.left = '-9999px';
|
|
312
|
+
}
|
|
313
|
+
if (dismissOverlay && dismissOverlay.parentNode) {
|
|
314
|
+
dismissOverlay.parentNode.removeChild(dismissOverlay);
|
|
315
|
+
}
|
|
316
|
+
dismissOverlay = null;
|
|
317
|
+
if (toggle) {
|
|
318
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
319
|
+
}
|
|
320
|
+
const shouldFocus = focusAnchor && toggle && typeof toggle.focus === 'function';
|
|
321
|
+
openMenuContext = null;
|
|
322
|
+
if (shouldFocus) {
|
|
323
|
+
try { toggle.focus(); } catch (_) {}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const renderSoundMenu = (context, { options, loading } = {}) => {
|
|
328
|
+
if (!context) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const effectiveOptions = Array.isArray(options) && options.length > 0
|
|
332
|
+
? options
|
|
333
|
+
: (soundOptionsCache && soundOptionsCache.length > 0 ? soundOptionsCache : baseSoundOptions());
|
|
334
|
+
if (!soundMenuContent) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const selectedChoice = globalSoundPreference?.choice || SOUND_DEFAULT_CHOICE;
|
|
338
|
+
const tabEnabled = context.state ? Boolean(context.state.notifyEnabled) : true;
|
|
339
|
+
const previousActive = (soundMenuContent.contains(document.activeElement) && document.activeElement instanceof HTMLElement)
|
|
340
|
+
? document.activeElement.getAttribute('data-menu-key')
|
|
341
|
+
: context.focusKey || null;
|
|
342
|
+
|
|
343
|
+
const soundItems = effectiveOptions.map((option) => {
|
|
344
|
+
const value = option.value;
|
|
345
|
+
const key = getMenuKeyForSoundValue(value);
|
|
346
|
+
const isSelected = value === selectedChoice
|
|
347
|
+
|| (value === SOUND_DEFAULT_CHOICE && (selectedChoice === SOUND_DEFAULT_CHOICE || !selectedChoice));
|
|
348
|
+
const label = option.label || 'Sound';
|
|
349
|
+
const meta = value === SOUND_SILENT_CHOICE ? 'No sound' : (value === SOUND_DEFAULT_CHOICE ? 'Default' : null);
|
|
350
|
+
const safeValue = escapeHtml(value);
|
|
351
|
+
const safeLabel = escapeHtml(label);
|
|
352
|
+
const safeMeta = meta ? escapeHtml(meta) : '';
|
|
353
|
+
const safeKey = escapeHtml(key);
|
|
354
|
+
return `
|
|
355
|
+
<button type="button" class="pinokio-notify-item" data-menu-item="true" data-role="sound-option" data-sound-value="${safeValue}" data-menu-key="${safeKey}" role="menuitemradio" aria-checked="${isSelected ? 'true' : 'false'}" ${isSelected ? 'data-selected="true"' : ''}>
|
|
356
|
+
<span class="pinokio-notify-item-icon">${isSelected ? '<i class="fa-solid fa-check"></i>' : ''}</span>
|
|
357
|
+
<span class="pinokio-notify-item-label">${safeLabel}</span>
|
|
358
|
+
${meta ? `<span class="pinokio-notify-item-meta">${safeMeta}</span>` : ''}
|
|
359
|
+
</button>
|
|
360
|
+
`;
|
|
361
|
+
}).join('');
|
|
362
|
+
|
|
363
|
+
const loadingRow = loading ? '<div class="pinokio-notify-loading">Loading sounds…</div>' : '';
|
|
364
|
+
|
|
365
|
+
soundMenuContent.innerHTML = `
|
|
366
|
+
<div class="pinokio-notify-section">
|
|
367
|
+
<button type="button" class="pinokio-notify-item" data-menu-item="true" data-role="toggle" data-menu-key="${MENU_KEY_TOGGLE}" role="menuitemcheckbox" aria-checked="${tabEnabled ? 'true' : 'false'}">
|
|
368
|
+
<span class="pinokio-notify-item-icon"><i class="fa-solid ${tabEnabled ? 'fa-bell' : 'fa-bell-slash'}"></i></span>
|
|
369
|
+
<span class="pinokio-notify-item-label">Notifications ${tabEnabled ? 'on' : 'off'}</span>
|
|
370
|
+
<span class="pinokio-notify-item-meta">This tab</span>
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="pinokio-notify-divider" role="presentation"></div>
|
|
374
|
+
<div class="pinokio-notify-section" role="group" aria-label="Notification sound">${soundItems}${loadingRow}</div>
|
|
375
|
+
<p class="pinokio-notify-hint">Sound applies to all tabs</p>
|
|
376
|
+
`;
|
|
377
|
+
|
|
378
|
+
const items = getMenuItems();
|
|
379
|
+
if (!items.length) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let focusTarget = items.find((item) => item.getAttribute('data-menu-key') === previousActive);
|
|
384
|
+
if (!focusTarget) {
|
|
385
|
+
focusTarget = items[0];
|
|
386
|
+
}
|
|
387
|
+
items.forEach((item) => {
|
|
388
|
+
item.setAttribute('tabindex', item === focusTarget ? '0' : '-1');
|
|
389
|
+
});
|
|
390
|
+
const shouldFocus = context.menuJustOpened
|
|
391
|
+
|| !soundMenuContent.contains(document.activeElement)
|
|
392
|
+
|| (focusTarget && document.activeElement !== focusTarget);
|
|
393
|
+
if (shouldFocus && focusTarget && typeof focusTarget.focus === 'function') {
|
|
394
|
+
focusTarget.focus();
|
|
395
|
+
}
|
|
396
|
+
context.menuJustOpened = false;
|
|
397
|
+
context.focusKey = focusTarget ? focusTarget.getAttribute('data-menu-key') : null;
|
|
398
|
+
context.focusIndex = items.indexOf(focusTarget);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const applySoundSelection = (value) => {
|
|
402
|
+
const choice = normaliseSoundChoice(value);
|
|
403
|
+
const previous = globalSoundPreference.choice;
|
|
404
|
+
globalSoundPreference.choice = choice;
|
|
405
|
+
if (previous !== choice) {
|
|
406
|
+
persistSoundPreference();
|
|
407
|
+
}
|
|
408
|
+
if (openMenuContext) {
|
|
409
|
+
openMenuContext.focusKey = getMenuKeyForSoundValue(choice);
|
|
410
|
+
}
|
|
411
|
+
playSoundPreview(choice);
|
|
412
|
+
if (openMenuContext) {
|
|
413
|
+
renderSoundMenu(openMenuContext);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const handleMenuClick = (event) => {
|
|
418
|
+
if (!openMenuContext) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const target = event.target instanceof HTMLElement ? event.target : null;
|
|
422
|
+
if (!target) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const item = target.closest('[data-menu-item="true"]');
|
|
426
|
+
if (!(item instanceof HTMLElement) || !soundMenuContent || !soundMenuContent.contains(item)) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const role = item.getAttribute('data-role');
|
|
430
|
+
if (role === 'toggle') {
|
|
431
|
+
event.preventDefault();
|
|
432
|
+
event.stopPropagation();
|
|
433
|
+
if (typeof openMenuContext.onToggle === 'function') {
|
|
434
|
+
openMenuContext.onToggle();
|
|
435
|
+
}
|
|
436
|
+
closeSoundMenu(true);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (role === 'sound-option') {
|
|
440
|
+
event.preventDefault();
|
|
441
|
+
const value = item.getAttribute('data-sound-value');
|
|
442
|
+
if (value) {
|
|
443
|
+
applySoundSelection(value);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const focusMenuItemByIndex = (index) => {
|
|
449
|
+
const items = getMenuItems();
|
|
450
|
+
if (!items.length) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
let nextIndex = index;
|
|
454
|
+
if (!Number.isInteger(nextIndex)) {
|
|
455
|
+
nextIndex = 0;
|
|
456
|
+
}
|
|
457
|
+
if (nextIndex < 0) {
|
|
458
|
+
nextIndex = 0;
|
|
459
|
+
}
|
|
460
|
+
if (nextIndex >= items.length) {
|
|
461
|
+
nextIndex = items.length - 1;
|
|
462
|
+
}
|
|
463
|
+
const target = items[nextIndex];
|
|
464
|
+
items.forEach((item, idx) => {
|
|
465
|
+
item.setAttribute('tabindex', idx === nextIndex ? '0' : '-1');
|
|
466
|
+
});
|
|
467
|
+
openMenuContext.focusIndex = nextIndex;
|
|
468
|
+
openMenuContext.focusKey = target ? target.getAttribute('data-menu-key') : null;
|
|
469
|
+
if (target && typeof target.focus === 'function') {
|
|
470
|
+
target.focus();
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const focusMenuItemByDelta = (delta) => {
|
|
475
|
+
const items = getMenuItems();
|
|
476
|
+
if (!items.length) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const current = document.activeElement && items.includes(document.activeElement)
|
|
480
|
+
? items.indexOf(document.activeElement)
|
|
481
|
+
: (Number.isInteger(openMenuContext?.focusIndex) ? openMenuContext.focusIndex : 0);
|
|
482
|
+
let nextIndex = current + delta;
|
|
483
|
+
if (nextIndex < 0) {
|
|
484
|
+
nextIndex = items.length - 1;
|
|
485
|
+
} else if (nextIndex >= items.length) {
|
|
486
|
+
nextIndex = 0;
|
|
487
|
+
}
|
|
488
|
+
focusMenuItemByIndex(nextIndex);
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const focusMenuItemByKey = (key) => {
|
|
492
|
+
const items = getMenuItems();
|
|
493
|
+
if (!items.length) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const target = items.find((item) => item.getAttribute('data-menu-key') === key);
|
|
497
|
+
if (target) {
|
|
498
|
+
focusMenuItemByIndex(items.indexOf(target));
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const handleMenuKeydown = (event) => {
|
|
503
|
+
if (!openMenuContext) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (event.key === 'ArrowDown') {
|
|
507
|
+
event.preventDefault();
|
|
508
|
+
focusMenuItemByDelta(1);
|
|
509
|
+
} else if (event.key === 'ArrowUp') {
|
|
510
|
+
event.preventDefault();
|
|
511
|
+
focusMenuItemByDelta(-1);
|
|
512
|
+
} else if (event.key === 'Home') {
|
|
513
|
+
event.preventDefault();
|
|
514
|
+
focusMenuItemByIndex(0);
|
|
515
|
+
} else if (event.key === 'End') {
|
|
516
|
+
event.preventDefault();
|
|
517
|
+
focusMenuItemByIndex(getMenuItems().length - 1);
|
|
518
|
+
} else if (event.key === 'Escape') {
|
|
519
|
+
event.preventDefault();
|
|
520
|
+
closeSoundMenu(true);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const ensureSoundMenuNode = () => {
|
|
525
|
+
if (soundMenuNode && soundMenuNode.parentNode) {
|
|
526
|
+
return soundMenuNode;
|
|
527
|
+
}
|
|
528
|
+
soundMenuNode = document.createElement('div');
|
|
529
|
+
soundMenuNode.className = 'pinokio-notify-popover';
|
|
530
|
+
soundMenuNode.id = 'pinokio-notify-popover';
|
|
531
|
+
soundMenuNode.setAttribute('aria-hidden', 'true');
|
|
532
|
+
soundMenuNode.style.top = '-9999px';
|
|
533
|
+
soundMenuNode.style.left = '-9999px';
|
|
534
|
+
soundMenuContent = document.createElement('div');
|
|
535
|
+
soundMenuContent.className = 'pinokio-notify-menu';
|
|
536
|
+
soundMenuContent.setAttribute('role', 'menu');
|
|
537
|
+
soundMenuContent.setAttribute('tabindex', '-1');
|
|
538
|
+
soundMenuNode.appendChild(soundMenuContent);
|
|
539
|
+
document.body.appendChild(soundMenuNode);
|
|
540
|
+
soundMenuNode.addEventListener('click', handleMenuClick);
|
|
541
|
+
soundMenuContent.addEventListener('keydown', handleMenuKeydown);
|
|
542
|
+
return soundMenuNode;
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const openSoundMenu = (toggle, frameName, state, onToggle) => {
|
|
546
|
+
if (!toggle) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (openMenuContext && openMenuContext.toggle === toggle) {
|
|
550
|
+
closeSoundMenu();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
closeSoundMenu();
|
|
554
|
+
const menu = ensureSoundMenuNode();
|
|
555
|
+
openMenuContext = {
|
|
556
|
+
toggle,
|
|
557
|
+
frameName,
|
|
558
|
+
state,
|
|
559
|
+
onToggle,
|
|
560
|
+
focusKey: MENU_KEY_TOGGLE,
|
|
561
|
+
focusIndex: 0,
|
|
562
|
+
menuJustOpened: true,
|
|
563
|
+
};
|
|
564
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
565
|
+
menu.classList.add('is-open');
|
|
566
|
+
menu.setAttribute('aria-hidden', 'false');
|
|
567
|
+
menu.style.visibility = 'hidden';
|
|
568
|
+
if (!dismissOverlay) {
|
|
569
|
+
dismissOverlay = document.createElement('div');
|
|
570
|
+
dismissOverlay.className = 'pinokio-notify-overlay';
|
|
571
|
+
dismissOverlay.setAttribute('role', 'presentation');
|
|
572
|
+
dismissOverlay.setAttribute('aria-hidden', 'true');
|
|
573
|
+
dismissOverlay.addEventListener('click', () => {
|
|
574
|
+
closeSoundMenu();
|
|
575
|
+
});
|
|
576
|
+
dismissOverlay.addEventListener('mousedown', (event) => {
|
|
577
|
+
event.preventDefault();
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
if (!dismissOverlay.parentNode) {
|
|
581
|
+
document.body.appendChild(dismissOverlay);
|
|
582
|
+
}
|
|
583
|
+
if (dismissOverlay instanceof HTMLElement) {
|
|
584
|
+
dismissOverlay.style.pointerEvents = 'auto';
|
|
585
|
+
}
|
|
586
|
+
renderSoundMenu(openMenuContext, {
|
|
587
|
+
options: soundOptionsCache && soundOptionsCache.length ? soundOptionsCache : baseSoundOptions(),
|
|
588
|
+
loading: !soundOptionsCache,
|
|
589
|
+
});
|
|
590
|
+
positionSoundMenu(menu, toggle);
|
|
591
|
+
menu.style.visibility = '';
|
|
592
|
+
updateMenuPosition();
|
|
593
|
+
loadSoundOptions().then((options) => {
|
|
594
|
+
if (!openMenuContext || openMenuContext.toggle !== toggle) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
renderSoundMenu(openMenuContext, { options, loading: false });
|
|
598
|
+
updateMenuPosition();
|
|
599
|
+
}).catch(() => {});
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const handleDocumentPointerDown = (event) => {
|
|
603
|
+
if (!openMenuContext) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const target = event.target;
|
|
607
|
+
if (!(target instanceof Node)) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (soundMenuNode && soundMenuNode.contains(target)) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const toggle = openMenuContext.toggle;
|
|
614
|
+
if (toggle && toggle.contains && toggle.contains(target)) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
closeSoundMenu();
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const handleDocumentKeydown = (event) => {
|
|
621
|
+
if (event.key === 'Escape' && openMenuContext) {
|
|
622
|
+
event.preventDefault();
|
|
623
|
+
closeSoundMenu(true);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const handleViewportChange = () => {
|
|
628
|
+
updateMenuPosition();
|
|
629
|
+
};
|
|
630
|
+
|
|
57
631
|
const getPreference = (frameName) => {
|
|
58
632
|
if (!frameName) {
|
|
59
633
|
return true;
|
|
@@ -110,6 +684,7 @@
|
|
|
110
684
|
};
|
|
111
685
|
|
|
112
686
|
hydratePreferences();
|
|
687
|
+
hydrateSoundPreference();
|
|
113
688
|
|
|
114
689
|
let ensureIndicatorObservers;
|
|
115
690
|
|
|
@@ -289,6 +864,93 @@
|
|
|
289
864
|
outline: 2px solid var(--pinokio-focus-color, #4c9afe);
|
|
290
865
|
outline-offset: 2px;
|
|
291
866
|
}
|
|
867
|
+
.${TOGGLE_CLASS}[aria-expanded="true"] {
|
|
868
|
+
color: var(--pinokio-focus-color, #4c9afe);
|
|
869
|
+
}
|
|
870
|
+
.pinokio-notify-popover {
|
|
871
|
+
position: absolute;
|
|
872
|
+
z-index: 2147482000;
|
|
873
|
+
min-width: 220px;
|
|
874
|
+
max-width: 280px;
|
|
875
|
+
color: #f8fafc;
|
|
876
|
+
background: rgba(15, 23, 42, 0.97);
|
|
877
|
+
border-radius: 10px;
|
|
878
|
+
box-shadow: 0 16px 32px rgba(15, 23, 42, 0.45);
|
|
879
|
+
padding: 8px;
|
|
880
|
+
display: none;
|
|
881
|
+
}
|
|
882
|
+
.pinokio-notify-popover.is-open {
|
|
883
|
+
display: block;
|
|
884
|
+
}
|
|
885
|
+
.pinokio-notify-menu {
|
|
886
|
+
display: flex;
|
|
887
|
+
flex-direction: column;
|
|
888
|
+
gap: 4px;
|
|
889
|
+
outline: none;
|
|
890
|
+
}
|
|
891
|
+
.pinokio-notify-section {
|
|
892
|
+
display: flex;
|
|
893
|
+
flex-direction: column;
|
|
894
|
+
gap: 4px;
|
|
895
|
+
}
|
|
896
|
+
.pinokio-notify-divider {
|
|
897
|
+
height: 1px;
|
|
898
|
+
background: rgba(148, 163, 184, 0.15);
|
|
899
|
+
margin: 4px 0;
|
|
900
|
+
}
|
|
901
|
+
.pinokio-notify-item {
|
|
902
|
+
display: flex;
|
|
903
|
+
align-items: center;
|
|
904
|
+
gap: 10px;
|
|
905
|
+
width: 100%;
|
|
906
|
+
padding: 8px 10px;
|
|
907
|
+
border: none;
|
|
908
|
+
border-radius: 7px;
|
|
909
|
+
background: transparent;
|
|
910
|
+
color: inherit;
|
|
911
|
+
text-align: left;
|
|
912
|
+
font: inherit;
|
|
913
|
+
cursor: pointer;
|
|
914
|
+
}
|
|
915
|
+
.pinokio-notify-item:hover,
|
|
916
|
+
.pinokio-notify-item:focus-visible {
|
|
917
|
+
background: rgba(148, 163, 184, 0.12);
|
|
918
|
+
}
|
|
919
|
+
.pinokio-notify-item[data-selected="true"] {
|
|
920
|
+
background: rgba(148, 163, 184, 0.18);
|
|
921
|
+
}
|
|
922
|
+
.pinokio-notify-item .pinokio-notify-item-icon {
|
|
923
|
+
width: 16px;
|
|
924
|
+
height: 16px;
|
|
925
|
+
display: flex;
|
|
926
|
+
align-items: center;
|
|
927
|
+
justify-content: center;
|
|
928
|
+
font-size: 13px;
|
|
929
|
+
}
|
|
930
|
+
.pinokio-notify-item .pinokio-notify-item-meta {
|
|
931
|
+
margin-left: auto;
|
|
932
|
+
font-size: 12px;
|
|
933
|
+
opacity: 0.75;
|
|
934
|
+
}
|
|
935
|
+
.pinokio-notify-hint {
|
|
936
|
+
margin: 2px 2px 0;
|
|
937
|
+
font-size: 11px;
|
|
938
|
+
color: rgba(148, 163, 184, 0.75);
|
|
939
|
+
}
|
|
940
|
+
.pinokio-notify-loading {
|
|
941
|
+
display: flex;
|
|
942
|
+
align-items: center;
|
|
943
|
+
gap: 8px;
|
|
944
|
+
padding: 6px 10px;
|
|
945
|
+
font-size: 12px;
|
|
946
|
+
color: rgba(148, 163, 184, 0.9);
|
|
947
|
+
}
|
|
948
|
+
.pinokio-notify-overlay {
|
|
949
|
+
position: fixed;
|
|
950
|
+
inset: 0;
|
|
951
|
+
z-index: 2147481995;
|
|
952
|
+
background: transparent;
|
|
953
|
+
}
|
|
292
954
|
`; // style injection for notify toggle
|
|
293
955
|
document.head.appendChild(style);
|
|
294
956
|
toggleStylesInjected = true;
|
|
@@ -304,6 +966,8 @@ const syncToggleAppearance = (toggle, enabled) => {
|
|
|
304
966
|
icon.classList.toggle('fa-bell-slash', !enabled);
|
|
305
967
|
toggle.dataset.enabled = enabled ? 'true' : 'false';
|
|
306
968
|
toggle.setAttribute('aria-pressed', enabled ? 'true' : 'false');
|
|
969
|
+
toggle.setAttribute('aria-haspopup', 'menu');
|
|
970
|
+
toggle.setAttribute('aria-expanded', (openMenuContext && openMenuContext.toggle === toggle) ? 'true' : 'false');
|
|
307
971
|
toggle.setAttribute('title', enabled ? 'Desktop notifications enabled' : 'Desktop notifications disabled');
|
|
308
972
|
toggle.setAttribute('aria-label', enabled ? 'Disable desktop notifications for this tab' : 'Enable desktop notifications for this tab');
|
|
309
973
|
};
|
|
@@ -360,6 +1024,7 @@ const syncToggleAppearance = (toggle, enabled) => {
|
|
|
360
1024
|
toggle.className = TOGGLE_CLASS;
|
|
361
1025
|
toggle.setAttribute('role', 'button');
|
|
362
1026
|
toggle.setAttribute('tabindex', '0');
|
|
1027
|
+
toggle.setAttribute('aria-controls', 'pinokio-notify-popover');
|
|
363
1028
|
const icon = document.createElement('i');
|
|
364
1029
|
toggle.appendChild(icon);
|
|
365
1030
|
(tab.querySelector(`.${TAB_MAIN_CLASS}`) || tab).appendChild(toggle);
|
|
@@ -376,19 +1041,33 @@ const syncToggleAppearance = (toggle, enabled) => {
|
|
|
376
1041
|
log('Notification preference changed', { frameName, enabled: next });
|
|
377
1042
|
};
|
|
378
1043
|
|
|
379
|
-
toggle.
|
|
1044
|
+
toggle._pinokioToggleActivate = activate;
|
|
1045
|
+
|
|
1046
|
+
const handleToggleInteraction = (event) => {
|
|
380
1047
|
event.preventDefault();
|
|
381
1048
|
event.stopPropagation();
|
|
382
|
-
|
|
1049
|
+
const current = getOrCreateState(frameName);
|
|
1050
|
+
if (!current) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
openSoundMenu(toggle, frameName, current, activate);
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
toggle.addEventListener('click', (event) => {
|
|
1057
|
+
handleToggleInteraction(event);
|
|
383
1058
|
});
|
|
384
1059
|
|
|
385
1060
|
toggle.addEventListener('keydown', (event) => {
|
|
386
1061
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
387
1062
|
event.preventDefault();
|
|
388
|
-
|
|
1063
|
+
handleToggleInteraction(event);
|
|
1064
|
+
} else if (event.key === 'Escape' && openMenuContext && openMenuContext.toggle === toggle) {
|
|
1065
|
+
event.preventDefault();
|
|
1066
|
+
closeSoundMenu(true);
|
|
389
1067
|
}
|
|
390
1068
|
});
|
|
391
1069
|
}
|
|
1070
|
+
toggle.setAttribute('aria-controls', 'pinokio-notify-popover');
|
|
392
1071
|
positionToggleWithinTab(tab, toggle);
|
|
393
1072
|
const container = tab.querySelector(`.${TAB_MAIN_CLASS}`) || tab;
|
|
394
1073
|
if (!containerObservers.has(container)) {
|
|
@@ -411,6 +1090,9 @@ const detachToggleForLink = (link) => {
|
|
|
411
1090
|
}
|
|
412
1091
|
const toggle = tab.querySelector(`.${TOGGLE_CLASS}`);
|
|
413
1092
|
if (toggle && toggle.parentNode) {
|
|
1093
|
+
if (openMenuContext && openMenuContext.toggle === toggle) {
|
|
1094
|
+
closeSoundMenu();
|
|
1095
|
+
}
|
|
414
1096
|
toggle.parentNode.removeChild(toggle);
|
|
415
1097
|
}
|
|
416
1098
|
const container = tab.querySelector(`.${TAB_MAIN_CLASS}`) || tab;
|
|
@@ -491,7 +1173,10 @@ const ensureTabAccessories = aggregateDebounce(() => {
|
|
|
491
1173
|
//subtitle,
|
|
492
1174
|
message,
|
|
493
1175
|
timeout: 60,
|
|
494
|
-
sound:
|
|
1176
|
+
sound: resolveNotificationSound(),
|
|
1177
|
+
// Target this notification to this browser/device only
|
|
1178
|
+
audience: 'device',
|
|
1179
|
+
device_id: (typeof window !== 'undefined' && typeof window.PinokioGetDeviceId === 'function') ? window.PinokioGetDeviceId() : undefined,
|
|
495
1180
|
};
|
|
496
1181
|
if (image) {
|
|
497
1182
|
payload.image = image;
|
|
@@ -682,6 +1367,11 @@ const ensureTabAccessories = aggregateDebounce(() => {
|
|
|
682
1367
|
ensureTabAccessories();
|
|
683
1368
|
treeObserver.observe(document.body, { childList: true, subtree: true });
|
|
684
1369
|
window.addEventListener('message', handleMessageEvent, true);
|
|
1370
|
+
document.addEventListener('mousedown', handleDocumentPointerDown, true);
|
|
1371
|
+
document.addEventListener('touchstart', handleDocumentPointerDown, true);
|
|
1372
|
+
document.addEventListener('keydown', handleDocumentKeydown, true);
|
|
1373
|
+
window.addEventListener('resize', handleViewportChange);
|
|
1374
|
+
window.addEventListener('scroll', handleViewportChange, true);
|
|
685
1375
|
window.addEventListener('storage', (event) => {
|
|
686
1376
|
if (event.key === DEBUG_STORAGE_KEY) {
|
|
687
1377
|
updateDebugFlag();
|
|
@@ -694,6 +1384,12 @@ const ensureTabAccessories = aggregateDebounce(() => {
|
|
|
694
1384
|
});
|
|
695
1385
|
ensureTabAccessories();
|
|
696
1386
|
log('Notification preferences refreshed from storage event');
|
|
1387
|
+
} else if (event.key === SOUND_PREF_STORAGE_KEY) {
|
|
1388
|
+
hydrateSoundPreference();
|
|
1389
|
+
if (openMenuContext) {
|
|
1390
|
+
renderSoundMenu(openMenuContext);
|
|
1391
|
+
}
|
|
1392
|
+
log('Sound preference refreshed from storage event');
|
|
697
1393
|
}
|
|
698
1394
|
});
|
|
699
1395
|
};
|