@heyputer/puter.js 2.0.0 → 2.0.1

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.
Files changed (74) hide show
  1. package/README.md +3 -1
  2. package/index.d.ts +479 -0
  3. package/package.json +15 -4
  4. package/APACHE_LICENSE.txt +0 -201
  5. package/doc/devlog.md +0 -49
  6. package/src/bg.png +0 -0
  7. package/src/bg.webp +0 -0
  8. package/src/lib/APICallLogger.js +0 -110
  9. package/src/lib/EventListener.js +0 -51
  10. package/src/lib/RequestError.js +0 -6
  11. package/src/lib/filesystem/APIFS.js +0 -73
  12. package/src/lib/filesystem/CacheFS.js +0 -243
  13. package/src/lib/filesystem/PostMessageFS.js +0 -40
  14. package/src/lib/filesystem/definitions.js +0 -39
  15. package/src/lib/path.js +0 -509
  16. package/src/lib/polyfills/localStorage.js +0 -92
  17. package/src/lib/polyfills/xhrshim.js +0 -233
  18. package/src/lib/socket.io/socket.io.esm.min.js +0 -7
  19. package/src/lib/socket.io/socket.io.esm.min.js.map +0 -1
  20. package/src/lib/socket.io/socket.io.js +0 -4385
  21. package/src/lib/socket.io/socket.io.js.map +0 -1
  22. package/src/lib/socket.io/socket.io.min.js +0 -7
  23. package/src/lib/socket.io/socket.io.min.js.map +0 -1
  24. package/src/lib/socket.io/socket.io.msgpack.min.js +0 -7
  25. package/src/lib/socket.io/socket.io.msgpack.min.js.map +0 -1
  26. package/src/lib/utils.js +0 -620
  27. package/src/lib/xdrpc.js +0 -104
  28. package/src/modules/AI.js +0 -680
  29. package/src/modules/Apps.js +0 -215
  30. package/src/modules/Auth.js +0 -171
  31. package/src/modules/Debug.js +0 -39
  32. package/src/modules/Drivers.js +0 -278
  33. package/src/modules/FSItem.js +0 -139
  34. package/src/modules/FileSystem/index.js +0 -187
  35. package/src/modules/FileSystem/operations/copy.js +0 -64
  36. package/src/modules/FileSystem/operations/deleteFSEntry.js +0 -59
  37. package/src/modules/FileSystem/operations/getReadUrl.js +0 -42
  38. package/src/modules/FileSystem/operations/mkdir.js +0 -62
  39. package/src/modules/FileSystem/operations/move.js +0 -75
  40. package/src/modules/FileSystem/operations/read.js +0 -46
  41. package/src/modules/FileSystem/operations/readdir.js +0 -102
  42. package/src/modules/FileSystem/operations/rename.js +0 -58
  43. package/src/modules/FileSystem/operations/sign.js +0 -103
  44. package/src/modules/FileSystem/operations/space.js +0 -40
  45. package/src/modules/FileSystem/operations/stat.js +0 -95
  46. package/src/modules/FileSystem/operations/symlink.js +0 -55
  47. package/src/modules/FileSystem/operations/upload.js +0 -440
  48. package/src/modules/FileSystem/operations/write.js +0 -65
  49. package/src/modules/FileSystem/utils/getAbsolutePathForApp.js +0 -21
  50. package/src/modules/Hosting.js +0 -138
  51. package/src/modules/KV.js +0 -301
  52. package/src/modules/OS.js +0 -95
  53. package/src/modules/Perms.js +0 -109
  54. package/src/modules/PuterDialog.js +0 -481
  55. package/src/modules/Threads.js +0 -75
  56. package/src/modules/UI.js +0 -1555
  57. package/src/modules/Util.js +0 -38
  58. package/src/modules/Workers.js +0 -120
  59. package/src/modules/networking/PSocket.js +0 -87
  60. package/src/modules/networking/PTLS.js +0 -100
  61. package/src/modules/networking/PWispHandler.js +0 -89
  62. package/src/modules/networking/parsers.js +0 -157
  63. package/src/modules/networking/requests.js +0 -282
  64. package/src/services/APIAccess.js +0 -46
  65. package/src/services/FSRelay.js +0 -20
  66. package/src/services/Filesystem.js +0 -122
  67. package/src/services/NoPuterYet.js +0 -20
  68. package/src/services/XDIncoming.js +0 -44
  69. package/test/ai.test.js +0 -214
  70. package/test/fs.test.js +0 -798
  71. package/test/index.html +0 -1183
  72. package/test/kv.test.js +0 -548
  73. package/test/txt2speech.test.js +0 -178
  74. package/webpack.config.js +0 -25
package/src/modules/UI.js DELETED
@@ -1,1555 +0,0 @@
1
- import FSItem from './FSItem.js';
2
- import PuterDialog from './PuterDialog.js';
3
- import EventListener from '../lib/EventListener.js';
4
- import putility from '@heyputer/putility';
5
-
6
- const FILE_SAVE_CANCELLED = Symbol('FILE_SAVE_CANCELLED');
7
- const FILE_OPEN_CANCELLED = Symbol('FILE_OPEN_CANCELLED');
8
-
9
- // AppConnection provides an API for interacting with another app.
10
- // It's returned by UI methods, and cannot be constructed directly by user code.
11
- // For basic usage:
12
- // - postMessage(message) Send a message to the target app
13
- // - on('message', callback) Listen to messages from the target app
14
- class AppConnection extends EventListener {
15
- // targetOrigin for postMessage() calls to Puter
16
- #puterOrigin = '*';
17
-
18
- // Whether the target app is open
19
- #isOpen;
20
-
21
- // Whether the target app uses the Puter SDK, and so accepts messages
22
- // (Closing and close events will still function.)
23
- #usesSDK;
24
-
25
- static from (values, context) {
26
- const connection = new AppConnection(context, {
27
- target: values.appInstanceID,
28
- usesSDK: values.usesSDK,
29
- });
30
-
31
- // When a connection is established the app is able to
32
- // provide some additional information about itself
33
- connection.response = values.response;
34
-
35
- return connection;
36
- }
37
-
38
- constructor(context, { target, usesSDK }) {
39
- super([
40
- 'message', // The target sent us something with postMessage()
41
- 'close', // The target app was closed
42
- ]);
43
- this.messageTarget = context.messageTarget;
44
- this.appInstanceID = context.appInstanceID;
45
- this.targetAppInstanceID = target;
46
- this.#isOpen = true;
47
- this.#usesSDK = usesSDK;
48
-
49
- this.log = context.puter.logger.fields({
50
- category: 'ipc',
51
- });
52
- this.log.fields({
53
- cons_source: context.appInstanceID,
54
- source: context.puter.appInstanceID,
55
- target,
56
- }).info(`AppConnection created to ${target}`, this);
57
-
58
- // TODO: Set this.#puterOrigin to the puter origin
59
-
60
- (globalThis.document) && window.addEventListener('message', event => {
61
- if (event.data.msg === 'messageToApp') {
62
- if (event.data.appInstanceID !== this.targetAppInstanceID) {
63
- // Message is from a different AppConnection; ignore it.
64
- return;
65
- }
66
- // TODO: does this check really make sense?
67
- if (event.data.targetAppInstanceID !== this.appInstanceID) {
68
- console.error(`AppConnection received message intended for wrong app! appInstanceID=${this.appInstanceID}, target=${event.data.targetAppInstanceID}`);
69
- return;
70
- }
71
- this.emit('message', event.data.contents);
72
- return;
73
- }
74
-
75
- if (event.data.msg === 'appClosed') {
76
- if (event.data.appInstanceID !== this.targetAppInstanceID) {
77
- // Message is from a different AppConnection; ignore it.
78
- return;
79
- }
80
-
81
- this.#isOpen = false;
82
- this.emit('close', {
83
- appInstanceID: this.targetAppInstanceID,
84
- statusCode: event.data.statusCode,
85
- });
86
- }
87
- });
88
- }
89
-
90
- // Does the target app use the Puter SDK? If not, certain features will be unavailable.
91
- get usesSDK() { return this.#usesSDK; }
92
-
93
- // Send a message to the target app. Requires the target to use the Puter SDK.
94
- postMessage(message) {
95
- if (!this.#isOpen) {
96
- console.warn('Trying to post message on a closed AppConnection');
97
- return;
98
- }
99
-
100
- if (!this.#usesSDK) {
101
- console.warn('Trying to post message to a non-SDK app');
102
- return;
103
- }
104
-
105
- this.messageTarget.postMessage({
106
- msg: 'messageToApp',
107
- appInstanceID: this.appInstanceID,
108
- targetAppInstanceID: this.targetAppInstanceID,
109
- // Note: there was a TODO comment here about specifying the origin,
110
- // but this should not happen here; the origin should be specified
111
- // on the other side where the expected origin for the app is known.
112
- targetAppOrigin: '*',
113
- contents: message,
114
- }, this.#puterOrigin);
115
- }
116
-
117
- // Attempt to close the target application
118
- close() {
119
- if (!this.#isOpen) {
120
- console.warn('Trying to close an app on a closed AppConnection');
121
- return;
122
- }
123
-
124
- this.messageTarget.postMessage({
125
- msg: 'closeApp',
126
- appInstanceID: this.appInstanceID,
127
- targetAppInstanceID: this.targetAppInstanceID,
128
- }, this.#puterOrigin);
129
- }
130
- }
131
-
132
- class UI extends EventListener {
133
- // Used to generate a unique message id for each message sent to the host environment
134
- // we start from 1 because 0 is falsy and we want to avoid that for the message id
135
- #messageID = 1;
136
-
137
- // Holds the callback functions for the various events
138
- // that are triggered when a watched item has changed.
139
- itemWatchCallbackFunctions = [];
140
-
141
- // Holds the unique app instance ID that is provided by the host environment
142
- appInstanceID;
143
-
144
- // Holds the unique app instance ID for the parent (if any), which is provided by the host environment
145
- parentInstanceID;
146
-
147
- // If we have a parent app, holds an AppConnection to it
148
- #parentAppConnection = null;
149
-
150
- // Holds the callback functions for the various events
151
- // that can be triggered by the host environment's messages.
152
- #callbackFunctions = [];
153
-
154
- // onWindowClose() is executed right before the window is closed. Users can override this function
155
- // to perform a variety of tasks right before window is closed. Users can override this function.
156
- #onWindowClose;
157
-
158
- // When an item is opened by this app in any way onItemsOpened() is executed. Users can override this function.
159
- #onItemsOpened;
160
-
161
- #onLaunchedWithItems;
162
-
163
- // List of events that can be listened to.
164
- #eventNames;
165
-
166
- // The most recent value that we received for a given broadcast, by name.
167
- #lastBroadcastValue = new Map(); // name -> data
168
-
169
- #overlayActive = false;
170
- #overlayTimer = null;
171
-
172
- // Replaces boilerplate for most methods: posts a message to the GUI with a unique ID, and sets a callback for it.
173
- #postMessageWithCallback = function(name, resolve, args = {}) {
174
- const msg_id = this.#messageID++;
175
- this.messageTarget?.postMessage({
176
- msg: name,
177
- env: this.env,
178
- appInstanceID: this.appInstanceID,
179
- uuid: msg_id,
180
- ...args,
181
- }, '*');
182
- //register callback
183
- this.#callbackFunctions[msg_id] = resolve;
184
- }
185
-
186
- #postMessageWithObject = function(name, value) {
187
- const dehydrator = this.util.rpc.getDehydrator({
188
- target: this.messageTarget
189
- });
190
- this.messageTarget?.postMessage({
191
- msg: name,
192
- env: this.env,
193
- appInstanceID: this.appInstanceID,
194
- value: dehydrator.dehydrate(value),
195
- }, '*');
196
- }
197
-
198
- #ipc_stub = async function ({
199
- callback,
200
- method,
201
- parameters,
202
- }) {
203
- let p, resolve;
204
- await new Promise(done_setting_resolve => {
205
- p = new Promise(resolve_ => {
206
- resolve = resolve_;
207
- done_setting_resolve();
208
- });
209
- });
210
- if ( ! resolve ) debugger;
211
- const callback_id = this.util.rpc.registerCallback(resolve);
212
- this.messageTarget?.postMessage({
213
- $: 'puter-ipc', v: 2,
214
- appInstanceID: this.appInstanceID,
215
- env: this.env,
216
- msg: method,
217
- parameters,
218
- uuid: callback_id,
219
- }, '*');
220
- const ret = await p;
221
- if ( callback ) callback(ret);
222
- return ret;
223
- }
224
-
225
- constructor (context, { appInstanceID, parentInstanceID }) {
226
- const eventNames = [
227
- 'localeChanged',
228
- 'themeChanged',
229
- 'connection',
230
- ];
231
- super(eventNames);
232
- this.#eventNames = eventNames;
233
- this.context = context;
234
- this.appInstanceID = appInstanceID;
235
- this.parentInstanceID = parentInstanceID;
236
- this.appID = context.appID;
237
- this.env = context.env;
238
- this.util = context.util;
239
-
240
- if(this.env === 'app'){
241
- this.messageTarget = window.parent;
242
- }
243
- else if(this.env === 'gui'){
244
- return;
245
- }
246
-
247
- // Context to pass to AppConnection instances
248
- this.context = this.context.sub({
249
- appInstanceID: this.appInstanceID,
250
- messageTarget: this.messageTarget,
251
- });
252
-
253
- if (this.parentInstanceID) {
254
- this.#parentAppConnection = new AppConnection(this.context, {
255
- target: this.parentInstanceID,
256
- usesSDK: true
257
- });
258
- }
259
-
260
- // Tell the host environment that this app is using the Puter SDK and is ready to receive messages,
261
- // this will allow the OS to send custom messages to the app
262
- this.messageTarget?.postMessage({
263
- msg: "READY",
264
- appInstanceID: this.appInstanceID,
265
- }, '*');
266
-
267
- // When this app's window is focused send a message to the host environment
268
- (globalThis.document) && window.addEventListener('focus', (e) => {
269
- this.messageTarget?.postMessage({
270
- msg: "windowFocused",
271
- appInstanceID: this.appInstanceID,
272
- }, '*');
273
- });
274
-
275
- // Bind the message event listener to the window
276
- let lastDraggedOverElement = null;
277
- (globalThis.document) && window.addEventListener('message', async (e) => {
278
- // `error`
279
- if(e.data.error){
280
- throw e.data.error;
281
- }
282
- // `focus` event
283
- else if(e.data.msg && e.data.msg === 'focus'){
284
- window.focus();
285
- }
286
- // `click` event
287
- else if(e.data.msg && e.data.msg === 'click'){
288
- // Get the element that was clicked on and click it
289
- const clicked_el = document.elementFromPoint(e.data.x, e.data.y);
290
- if(clicked_el !== null)
291
- clicked_el.click();
292
- }
293
- // `dragover` event based on the `drag` event from the host environment
294
- else if(e.data.msg && e.data.msg === 'drag'){
295
- // Get the element being dragged over
296
- const draggedOverElement = document.elementFromPoint(e.data.x, e.data.y);
297
- if(draggedOverElement !== lastDraggedOverElement){
298
- // If the last element exists and is different from the current, dispatch a dragleave on it
299
- if(lastDraggedOverElement){
300
- const dragLeaveEvent = new Event('dragleave', {
301
- bubbles: true,
302
- cancelable: true,
303
- clientX: e.data.x,
304
- clientY: e.data.y
305
- });
306
- lastDraggedOverElement.dispatchEvent(dragLeaveEvent);
307
- }
308
- // If the current element exists and is different from the last, dispatch dragenter on it
309
- if(draggedOverElement){
310
- const dragEnterEvent = new Event('dragenter', {
311
- bubbles: true,
312
- cancelable: true,
313
- clientX: e.data.x,
314
- clientY: e.data.y
315
- });
316
- draggedOverElement.dispatchEvent(dragEnterEvent);
317
- }
318
-
319
- // Update the lastDraggedOverElement
320
- lastDraggedOverElement = draggedOverElement;
321
- }
322
- }
323
- // `drop` event
324
- else if(e.data.msg && e.data.msg === 'drop'){
325
- if(lastDraggedOverElement){
326
- const dropEvent = new CustomEvent('drop', {
327
- bubbles: true,
328
- cancelable: true,
329
- detail: {
330
- clientX: e.data.x,
331
- clientY: e.data.y,
332
- items: e.data.items
333
- }
334
- });
335
- lastDraggedOverElement.dispatchEvent(dropEvent);
336
-
337
- // Reset the lastDraggedOverElement
338
- lastDraggedOverElement = null;
339
- }
340
- }
341
- // windowWillClose
342
- else if(e.data.msg === 'windowWillClose'){
343
- // If the user has not overridden onWindowClose() then send a message back to the host environment
344
- // to let it know that it is ok to close the window.
345
- if(this.#onWindowClose === undefined){
346
- this.messageTarget?.postMessage({
347
- msg: true,
348
- appInstanceID: this.appInstanceID,
349
- original_msg_id: e.data.msg_id,
350
- }, '*');
351
- }
352
- // If the user has overridden onWindowClose() then send a message back to the host environment
353
- // to let it know that it is NOT ok to close the window. Then execute onWindowClose() and the user will
354
- // have to manually close the window.
355
- else{
356
- this.messageTarget?.postMessage({
357
- msg: false,
358
- appInstanceID: this.appInstanceID,
359
- original_msg_id: e.data.msg_id,
360
- }, '*');
361
- this.#onWindowClose();
362
- }
363
- }
364
- // itemsOpened
365
- else if(e.data.msg === 'itemsOpened'){
366
- // If the user has not overridden onItemsOpened() then only send a message back to the host environment
367
- if(this.#onItemsOpened === undefined){
368
- this.messageTarget?.postMessage({
369
- msg: true,
370
- appInstanceID: this.appInstanceID,
371
- original_msg_id: e.data.msg_id,
372
- }, '*');
373
- }
374
- // If the user has overridden onItemsOpened() then send a message back to the host environment
375
- // and execute onItemsOpened()
376
- else{
377
- this.messageTarget?.postMessage({
378
- msg: false,
379
- appInstanceID: this.appInstanceID,
380
- original_msg_id: e.data.msg_id,
381
- }, '*');
382
-
383
- let items = [];
384
- if(e.data.items.length > 0){
385
- for (let index = 0; index < e.data.items.length; index++)
386
- items.push(new FSItem(e.data.items[index]))
387
- }
388
- this.#onItemsOpened(items);
389
- }
390
- }
391
- // getAppDataSucceeded
392
- else if(e.data.msg === 'getAppDataSucceeded'){
393
- let appDataItem = new FSItem(e.data.item);
394
- if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
395
- this.#callbackFunctions[e.data.original_msg_id](appDataItem);
396
- }
397
- }
398
- // readAppDataFileSucceeded
399
- else if(e.data.msg === 'readAppDataFileSucceeded'){
400
- let appDataItem = new FSItem(e.data.item);
401
- if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
402
- this.#callbackFunctions[e.data.original_msg_id](appDataItem);
403
- }
404
- }
405
- // readAppDataFileFailed
406
- else if(e.data.msg === 'readAppDataFileFailed'){
407
- if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
408
- this.#callbackFunctions[e.data.original_msg_id](null);
409
- }
410
- }
411
- // Determine if this is a response to a previous message and if so, is there
412
- // a callback function for this message? if answer is yes to both then execute the callback
413
- else if(e.data.original_msg_id !== undefined && this.#callbackFunctions[e.data.original_msg_id]){
414
- if(e.data.msg === 'fileOpenPicked'){
415
- // 1 item returned
416
- if(e.data.items.length === 1){
417
- this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.items[0]));
418
- }
419
- // multiple items returned
420
- else if(e.data.items.length > 1){
421
- // multiple items returned
422
- let items = [];
423
- for (let index = 0; index < e.data.items.length; index++)
424
- items.push(new FSItem(e.data.items[index]))
425
- this.#callbackFunctions[e.data.original_msg_id](items);
426
- }
427
- }
428
- else if(e.data.msg === 'directoryPicked'){
429
- // 1 item returned
430
- if(e.data.items.length === 1){
431
- this.#callbackFunctions[e.data.original_msg_id](new FSItem({
432
- uid: e.data.items[0].uid,
433
- name: e.data.items[0].fsentry_name,
434
- path: e.data.items[0].path,
435
- readURL: e.data.items[0].read_url,
436
- writeURL: e.data.items[0].write_url,
437
- metadataURL: e.data.items[0].metadata_url,
438
- isDirectory: true,
439
- size: e.data.items[0].fsentry_size,
440
- accessed: e.data.items[0].fsentry_accessed,
441
- modified: e.data.items[0].fsentry_modified,
442
- created: e.data.items[0].fsentry_created,
443
- }));
444
- }
445
- // multiple items returned
446
- else if(e.data.items.length > 1){
447
- // multiple items returned
448
- let items = [];
449
- for (let index = 0; index < e.data.items.length; index++)
450
- items.push(new FSItem(e.data.items[index]))
451
- this.#callbackFunctions[e.data.original_msg_id](items);
452
- }
453
- }
454
- else if(e.data.msg === 'colorPicked'){
455
- // execute callback
456
- this.#callbackFunctions[e.data.original_msg_id](e.data.color);
457
- }
458
- else if(e.data.msg === 'fontPicked'){
459
- // execute callback
460
- this.#callbackFunctions[e.data.original_msg_id](e.data.font);
461
- }
462
- else if(e.data.msg === 'alertResponded'){
463
- // execute callback
464
- this.#callbackFunctions[e.data.original_msg_id](e.data.response);
465
- }
466
- else if(e.data.msg === 'promptResponded'){
467
- // execute callback
468
- this.#callbackFunctions[e.data.original_msg_id](e.data.response);
469
- }
470
- else if(e.data.msg === 'languageReceived'){
471
- // execute callback
472
- this.#callbackFunctions[e.data.original_msg_id](e.data.language);
473
- }
474
- else if(e.data.msg === "fileSaved"){
475
- // execute callback
476
- this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.saved_file));
477
- }
478
- else if(e.data.msg === "fileSaveCancelled"){
479
- // execute callback
480
- this.#callbackFunctions[e.data.original_msg_id](FILE_SAVE_CANCELLED);
481
- }
482
- else if(e.data.msg === "fileOpenCancelled"){
483
- // execute callback
484
- this.#callbackFunctions[e.data.original_msg_id](FILE_OPEN_CANCELLED);
485
- }
486
- else{
487
- // execute callback
488
- this.#callbackFunctions[e.data.original_msg_id](e.data);
489
- }
490
-
491
- //remove this callback function since it won't be needed again
492
- delete this.#callbackFunctions[e.data.original_msg_id];
493
- }
494
- // Item Watch response
495
- else if(e.data.msg === "itemChanged" && e.data.data && e.data.data.uid){
496
- //excute callback
497
- if(this.itemWatchCallbackFunctions[e.data.data.uid] && typeof this.itemWatchCallbackFunctions[e.data.data.uid] === 'function')
498
- this.itemWatchCallbackFunctions[e.data.data.uid](e.data.data);
499
- }
500
- // Broadcasts
501
- else if (e.data.msg === 'broadcast') {
502
- const { name, data } = e.data;
503
- if (!this.#eventNames.includes(name)) {
504
- return;
505
- }
506
- this.emit(name, data);
507
- this.#lastBroadcastValue.set(name, data);
508
- }
509
- else if ( e.data.msg === 'connection' ) {
510
- e.data.usesSDK = true; // we can safely assume this
511
- const conn = AppConnection.from(e.data, this.context);
512
- const accept = value => {
513
- this.messageTarget?.postMessage({
514
- $: 'connection-resp',
515
- connection: e.data.appInstanceID,
516
- accept: true,
517
- value,
518
- }, '*');
519
- };
520
- const reject = value => {
521
- this.messageTarget?.postMessage({
522
- $: 'connection-resp',
523
- connection: e.data.appInstanceID,
524
- accept: false,
525
- value,
526
- }, '*');
527
- };
528
- this.emit('connection', {
529
- conn, accept, reject,
530
- });
531
- }
532
- });
533
-
534
- // We need to send the mouse position to the host environment
535
- // This is important since a lot of UI elements depend on the mouse position (e.g. ContextMenus, Tooltips, etc.)
536
- // and the host environment needs to know the mouse position to show these elements correctly.
537
- // The host environment can't just get the mouse position since when the mouse is over an iframe it
538
- // will not be able to get the mouse position. So we need to send the mouse position to the host environment.
539
- globalThis.document?.addEventListener('mousemove', async (event)=>{
540
- // Get the mouse position from the event object
541
- this.mouseX = event.clientX;
542
- this.mouseY = event.clientY;
543
-
544
- // send the mouse position to the host environment
545
- this.messageTarget?.postMessage({
546
- msg: "mouseMoved",
547
- appInstanceID: this.appInstanceID,
548
- x: this.mouseX,
549
- y: this.mouseY,
550
- }, '*');
551
- });
552
-
553
- // click
554
- globalThis.document?.addEventListener('click', async (event)=>{
555
- // Get the mouse position from the event object
556
- this.mouseX = event.clientX;
557
- this.mouseY = event.clientY;
558
-
559
- // send the mouse position to the host environment
560
- this.messageTarget?.postMessage({
561
- msg: "mouseClicked",
562
- appInstanceID: this.appInstanceID,
563
- x: this.mouseX,
564
- y: this.mouseY,
565
- }, '*');
566
- })
567
- }
568
-
569
- onWindowClose = function(callback) {
570
- this.#onWindowClose = callback;
571
- }
572
-
573
- onItemsOpened = function(callback) {
574
- // DEPRECATED - this is also called when items are dropped on the app, which in new versions should be handled
575
- // with the 'drop' event.
576
- // Check if a file was opened with this app, i.e. check URL parameters of window/iframe
577
- // Even though the file has been opened when the app is launched, we need to wait for the onItemsOpened callback to be set
578
- // before we can call it. This is why we need to check the URL parameters here.
579
- // This should also be done only the very first time the callback is set (hence the if(!this.#onItemsOpened) check) since
580
- // the URL parameters will be checked every time the callback is set which can cause problems if the callback is set multiple times.
581
- if(!this.#onItemsOpened){
582
- let URLParams = new URLSearchParams(globalThis.location.search);
583
- if(URLParams.has('puter.item.name') && URLParams.has('puter.item.uid') && URLParams.has('puter.item.read_url')){
584
- let fpath = URLParams.get('puter.item.path');
585
-
586
- if(!fpath.startsWith('~/') && !fpath.startsWith('/'))
587
- fpath = '~/' + fpath;
588
-
589
- callback([new FSItem({
590
- name: URLParams.get('puter.item.name'),
591
- path: fpath,
592
- uid: URLParams.get('puter.item.uid'),
593
- readURL: URLParams.get('puter.item.read_url'),
594
- writeURL: URLParams.get('puter.item.write_url'),
595
- metadataURL: URLParams.get('puter.item.metadata_url'),
596
- size: URLParams.get('puter.item.size'),
597
- accessed: URLParams.get('puter.item.accessed'),
598
- modified: URLParams.get('puter.item.modified'),
599
- created: URLParams.get('puter.item.created'),
600
- })]);
601
- }
602
- }
603
-
604
- this.#onItemsOpened = callback;
605
- }
606
-
607
- // Check if the app was launched with items
608
- // This is useful for apps that are launched with items (e.g. when a file is opened with the app)
609
- wasLaunchedWithItems = function() {
610
- const URLParams = new URLSearchParams(globalThis.location.search);
611
- return URLParams.has('puter.item.name') &&
612
- URLParams.has('puter.item.uid') &&
613
- URLParams.has('puter.item.read_url');
614
- }
615
-
616
- onLaunchedWithItems = function(callback) {
617
- // Check if a file was opened with this app, i.e. check URL parameters of window/iframe
618
- // Even though the file has been opened when the app is launched, we need to wait for the onLaunchedWithItems callback to be set
619
- // before we can call it. This is why we need to check the URL parameters here.
620
- // This should also be done only the very first time the callback is set (hence the if(!this.#onLaunchedWithItems) check) since
621
- // the URL parameters will be checked every time the callback is set which can cause problems if the callback is set multiple times.
622
- if(!this.#onLaunchedWithItems){
623
- let URLParams = new URLSearchParams(globalThis.location.search);
624
- if(URLParams.has('puter.item.name') && URLParams.has('puter.item.uid') && URLParams.has('puter.item.read_url')){
625
- let fpath = URLParams.get('puter.item.path');
626
-
627
- if(!fpath.startsWith('~/') && !fpath.startsWith('/'))
628
- fpath = '~/' + fpath;
629
-
630
- callback([new FSItem({
631
- name: URLParams.get('puter.item.name'),
632
- path: fpath,
633
- uid: URLParams.get('puter.item.uid'),
634
- readURL: URLParams.get('puter.item.read_url'),
635
- writeURL: URLParams.get('puter.item.write_url'),
636
- metadataURL: URLParams.get('puter.item.metadata_url'),
637
- size: URLParams.get('puter.item.size'),
638
- accessed: URLParams.get('puter.item.accessed'),
639
- modified: URLParams.get('puter.item.modified'),
640
- created: URLParams.get('puter.item.created'),
641
- })]);
642
- }
643
- }
644
-
645
- this.#onLaunchedWithItems = callback;
646
- }
647
-
648
- requestEmailConfirmation = function() {
649
- return new Promise((resolve, reject) => {
650
- this.#postMessageWithCallback('requestEmailConfirmation', resolve, { });
651
- });
652
- }
653
-
654
- alert = function(message, buttons, options, callback) {
655
- return new Promise((resolve) => {
656
- this.#postMessageWithCallback('ALERT', resolve, { message, buttons, options });
657
- })
658
- }
659
-
660
- socialShare = function(url, message, options, callback) {
661
- return new Promise((resolve) => {
662
- this.#postMessageWithCallback('socialShare', resolve, { url, message, options });
663
- })
664
- }
665
-
666
- prompt = function(message, placeholder, options, callback) {
667
- return new Promise((resolve) => {
668
- this.#postMessageWithCallback('PROMPT', resolve, { message, placeholder, options });
669
- })
670
- }
671
-
672
- showDirectoryPicker = function(options, callback){
673
- return new Promise((resolve, reject) => {
674
- if (!globalThis.open) {
675
- return reject("This API is not compatible in Web Workers.");
676
- }
677
- const msg_id = this.#messageID++;
678
- if(this.env === 'app'){
679
- this.messageTarget?.postMessage({
680
- msg: "showDirectoryPicker",
681
- appInstanceID: this.appInstanceID,
682
- uuid: msg_id,
683
- options: options,
684
- env: this.env,
685
- }, '*');
686
- }else{
687
- let w = 700;
688
- let h = 400;
689
- let title = 'Puter: Open Directory';
690
- var left = (screen.width/2)-(w/2);
691
- var top = (screen.height/2)-(h/2);
692
- window.open(`${puter.defaultGUIOrigin}/action/show-directory-picker?embedded_in_popup=true&msg_id=${msg_id}&appInstanceID=${this.appInstanceID}&env=${this.env}&options=${JSON.stringify(options)}`,
693
- title,
694
- 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
695
- }
696
-
697
- //register callback
698
- this.#callbackFunctions[msg_id] = resolve;
699
- })
700
- }
701
-
702
- showOpenFilePicker = function(options, callback){
703
- const undefinedOnCancel = new putility.libs.promise.TeePromise();
704
- const resolveOnlyPromise = new Promise((resolve, reject) => {
705
- if (!globalThis.open) {
706
- return reject("This API is not compatible in Web Workers.");
707
- }
708
- const msg_id = this.#messageID++;
709
-
710
- if(this.env === 'app'){
711
- this.messageTarget?.postMessage({
712
- msg: "showOpenFilePicker",
713
- appInstanceID: this.appInstanceID,
714
- uuid: msg_id,
715
- options: options ?? {},
716
- env: this.env,
717
- }, '*');
718
- }else{
719
- let w = 700;
720
- let h = 400;
721
- let title = 'Puter: Open File';
722
- var left = (screen.width/2)-(w/2);
723
- var top = (screen.height/2)-(h/2);
724
- window.open(`${puter.defaultGUIOrigin}/action/show-open-file-picker?embedded_in_popup=true&msg_id=${msg_id}&appInstanceID=${this.appInstanceID}&env=${this.env}&options=${JSON.stringify(options ?? {})}`,
725
- title,
726
- 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
727
- }
728
- //register callback
729
- this.#callbackFunctions[msg_id] = (maybe_result) => {
730
- // Only resolve cancel events if this was called with `.undefinedOnCancel`
731
- if ( maybe_result === FILE_OPEN_CANCELLED ) {
732
- undefinedOnCancel.resolve(undefined);
733
- return;
734
- }
735
- undefinedOnCancel.resolve(maybe_result);
736
- resolve(maybe_result);
737
- };
738
- })
739
- resolveOnlyPromise.undefinedOnCancel = undefinedOnCancel;
740
- return resolveOnlyPromise;
741
- }
742
-
743
- showFontPicker = function(options){
744
- return new Promise((resolve) => {
745
- this.#postMessageWithCallback('showFontPicker', resolve, { options: options ?? {} });
746
- })
747
- }
748
-
749
- showColorPicker = function(options){
750
- return new Promise((resolve) => {
751
- this.#postMessageWithCallback('showColorPicker', resolve, { options: options ?? {} });
752
- })
753
- }
754
-
755
- showSaveFilePicker = function(content, suggestedName, type){
756
- const undefinedOnCancel = new putility.libs.promise.TeePromise();
757
- const resolveOnlyPromise = new Promise((resolve, reject) => {
758
- if (!globalThis.open) {
759
- return reject("This API is not compatible in Web Workers.");
760
- }
761
- const msg_id = this.#messageID++;
762
- if ( ! type && Object.prototype.toString.call(content) === '[object URL]' ) {
763
- type = 'url';
764
- }
765
- const url = type === 'url' ? content.toString() : undefined;
766
- const source_path = ['move','copy'].includes(type) ? content : undefined;
767
-
768
- if(this.env === 'app'){
769
- this.messageTarget?.postMessage({
770
- msg: "showSaveFilePicker",
771
- appInstanceID: this.appInstanceID,
772
- content: url ? undefined : content,
773
- save_type: type,
774
- url,
775
- source_path,
776
- suggestedName: suggestedName ?? '',
777
- env: this.env,
778
- uuid: msg_id
779
- }, '*');
780
- }else{
781
- window.addEventListener('message', async (e) => {
782
- if(e.data?.msg === "sendMeFileData"){
783
- // Send the blob URL to the host environment
784
- e.source.postMessage({
785
- msg: "showSaveFilePickerPopup",
786
- content: url ? undefined : content,
787
- url: url ? url.toString() : undefined,
788
- suggestedName: suggestedName ?? '',
789
- env: this.env,
790
- uuid: msg_id
791
- }, '*');
792
-
793
- // remove the event listener
794
- window.removeEventListener('message', this);
795
- }
796
- });
797
- // Create a Blob from your binary data
798
- let blob = new Blob([content], {type: 'application/octet-stream'});
799
-
800
- // Create an object URL for the Blob
801
- let objectUrl = URL.createObjectURL(blob);
802
-
803
- let w = 700;
804
- let h = 400;
805
- let title = 'Puter: Save File';
806
- var left = (screen.width/2)-(w/2);
807
- var top = (screen.height/2)-(h/2);
808
- window.open(`${puter.defaultGUIOrigin}/action/show-save-file-picker?embedded_in_popup=true&msg_id=${msg_id}&appInstanceID=${this.appInstanceID}&env=${this.env}&blobUrl=${encodeURIComponent(objectUrl)}`,
809
- title,
810
- 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
811
- }
812
- //register callback
813
- this.#callbackFunctions[msg_id] = (maybe_result) => {
814
- // Only resolve cancel events if this was called with `.undefinedOnCancel`
815
- if ( maybe_result === FILE_SAVE_CANCELLED ) {
816
- undefinedOnCancel.resolve(undefined);
817
- return;
818
- }
819
- undefinedOnCancel.resolve(maybe_result);
820
- resolve(maybe_result);
821
- };
822
- });
823
-
824
- resolveOnlyPromise.undefinedOnCancel = undefinedOnCancel;
825
-
826
- return resolveOnlyPromise;
827
- }
828
-
829
- setWindowTitle = function(title, window_id, callback) {
830
- if(typeof window_id === 'function'){
831
- callback = window_id;
832
- window_id = undefined;
833
- }else if(typeof window_id === "object" && window_id !== null){
834
- window_id = window_id.id;
835
- }
836
-
837
- return new Promise((resolve) => {
838
- this.#postMessageWithCallback('setWindowTitle', resolve, { new_title: title, window_id: window_id});
839
- })
840
- }
841
-
842
- setWindowWidth = function(width, window_id, callback) {
843
- if(typeof window_id === 'function'){
844
- callback = window_id;
845
- window_id = undefined;
846
- }else if(typeof window_id === "object" && window_id !== null){
847
- window_id = window_id.id;
848
- }
849
-
850
- return new Promise((resolve) => {
851
- this.#postMessageWithCallback('setWindowWidth', resolve, { width: width, window_id: window_id });
852
- })
853
- }
854
-
855
- setWindowHeight = function(height, window_id, callback) {
856
- if(typeof window_id === 'function'){
857
- callback = window_id;
858
- window_id = undefined;
859
- }else if(typeof window_id === "object" && window_id !== null){
860
- window_id = window_id.id;
861
- }
862
-
863
- return new Promise((resolve) => {
864
- this.#postMessageWithCallback('setWindowHeight', resolve, { height: height, window_id: window_id });
865
- })
866
- }
867
-
868
- setWindowSize = function(width, height, window_id, callback) {
869
- if(typeof window_id === 'function'){
870
- callback = window_id;
871
- window_id = undefined;
872
- }else if(typeof window_id === "object" && window_id !== null){
873
- window_id = window_id.id;
874
- }
875
-
876
- return new Promise((resolve) => {
877
- this.#postMessageWithCallback('setWindowSize', resolve, { width: width, height: height, window_id: window_id });
878
- })
879
- }
880
-
881
- setWindowPosition = function(x, y, window_id, callback) {
882
- if(typeof window_id === 'function'){
883
- callback = window_id;
884
- window_id = undefined;
885
- }else if(typeof window_id === "object" && window_id !== null){
886
- window_id = window_id.id;
887
- }
888
-
889
- return new Promise((resolve) => {
890
- this.#postMessageWithCallback('setWindowPosition', resolve, { x, y, window_id });
891
- })
892
- }
893
-
894
- setWindowY = function(y, window_id, callback) {
895
- if(typeof window_id === 'function'){
896
- callback = window_id;
897
- window_id = undefined;
898
- }else if(typeof window_id === "object" && window_id !== null){
899
- window_id = window_id.id;
900
- }
901
-
902
- return new Promise((resolve) => {
903
- this.#postMessageWithCallback('setWindowY', resolve, { y, window_id });
904
- })
905
- }
906
-
907
- setWindowX = function(x, window_id, callback) {
908
- if(typeof window_id === 'function'){
909
- callback = window_id;
910
- window_id = undefined;
911
- }else if(typeof window_id === "object" && window_id !== null){
912
- window_id = window_id.id;
913
- }
914
-
915
- return new Promise((resolve) => {
916
- this.#postMessageWithCallback('setWindowX', resolve, { x, window_id });
917
- })
918
- }
919
-
920
- setMenubar = function(spec) {
921
- this.#postMessageWithObject('setMenubar', spec);
922
- }
923
-
924
- requestPermission = function(options) {
925
- return new Promise((resolve) => {
926
- if (this.env === 'app') {
927
- return new Promise((resolve) => {
928
- this.#postMessageWithCallback('requestPermission', resolve, { options });
929
- })
930
- } else {
931
- // TODO: Implement for web
932
- resolve(false);
933
- }
934
- })
935
- }
936
-
937
- disableMenuItem = function(item_id) {
938
- this.#postMessageWithObject('disableMenuItem', {id: item_id});
939
- }
940
-
941
- enableMenuItem = function(item_id) {
942
- this.#postMessageWithObject('enableMenuItem', {id: item_id});
943
- }
944
-
945
- setMenuItemIcon = function(item_id, icon) {
946
- this.#postMessageWithObject('setMenuItemIcon', {id: item_id, icon: icon});
947
- }
948
-
949
- setMenuItemIconActive = function(item_id, icon) {
950
- this.#postMessageWithObject('setMenuItemIconActive', {id: item_id, icon: icon});
951
- }
952
-
953
- setMenuItemChecked = function(item_id, checked) {
954
- this.#postMessageWithObject('setMenuItemChecked', {id: item_id, checked: checked});
955
- }
956
-
957
- contextMenu = function(spec) {
958
- this.#postMessageWithObject('contextMenu', spec);
959
- }
960
-
961
- /**
962
- * Asynchronously extracts entries from DataTransferItems, like files and directories.
963
- *
964
- * @private
965
- * @function
966
- * @async
967
- * @param {DataTransferItemList} dataTransferItems - List of data transfer items from a drag-and-drop operation.
968
- * @param {Object} [options={}] - Optional settings.
969
- * @param {boolean} [options.raw=false] - Determines if the file path should be processed.
970
- * @returns {Promise<Array<File|Entry>>} - A promise that resolves to an array of File or Entry objects.
971
- * @throws {Error} - Throws an error if there's an EncodingError and provides information about how to solve it.
972
- *
973
- * @example
974
- * const items = event.dataTransfer.items;
975
- * const entries = await getEntriesFromDataTransferItems(items, { raw: false });
976
- */
977
- getEntriesFromDataTransferItems = async function(dataTransferItems, options = { raw: false }) {
978
- const checkErr = (err) => {
979
- if (this.getEntriesFromDataTransferItems.didShowInfo) return
980
- if (err.name !== 'EncodingError') return
981
- this.getEntriesFromDataTransferItems.didShowInfo = true
982
- const infoMsg = `${err.name} occurred within datatransfer-files-promise module\n`
983
- + `Error message: "${err.message}"\n`
984
- + 'Try serving html over http if currently you are running it from the filesystem.'
985
- console.warn(infoMsg)
986
- }
987
-
988
- const readFile = (entry, path = '') => {
989
- return new Promise((resolve, reject) => {
990
- entry.file(file => {
991
- if (!options.raw) file.filepath = path + file.name // save full path
992
- resolve(file)
993
- }, (err) => {
994
- checkErr(err)
995
- reject(err)
996
- })
997
- })
998
- }
999
-
1000
- const dirReadEntries = (dirReader, path) => {
1001
- return new Promise((resolve, reject) => {
1002
- dirReader.readEntries(async entries => {
1003
- let files = []
1004
- for (let entry of entries) {
1005
- const itemFiles = await getFilesFromEntry(entry, path)
1006
- files = files.concat(itemFiles)
1007
- }
1008
- resolve(files)
1009
- }, (err) => {
1010
- checkErr(err)
1011
- reject(err)
1012
- })
1013
- })
1014
- }
1015
-
1016
- const readDir = async (entry, path) => {
1017
- const dirReader = entry.createReader()
1018
- const newPath = path + entry.name + '/'
1019
- let files = []
1020
- let newFiles
1021
- do {
1022
- newFiles = await dirReadEntries(dirReader, newPath)
1023
- files = files.concat(newFiles)
1024
- } while (newFiles.length > 0)
1025
- return files
1026
- }
1027
-
1028
- const getFilesFromEntry = async (entry, path = '') => {
1029
- if(entry === null)
1030
- return;
1031
- else if (entry.isFile) {
1032
- const file = await readFile(entry, path)
1033
- return [file]
1034
- }
1035
- else if (entry.isDirectory) {
1036
- const files = await readDir(entry, path)
1037
- files.push(entry)
1038
- return files
1039
- }
1040
- }
1041
-
1042
- let files = []
1043
- let entries = []
1044
-
1045
- // Pull out all entries before reading them
1046
- for (let i = 0, ii = dataTransferItems.length; i < ii; i++) {
1047
- entries.push(dataTransferItems[i].webkitGetAsEntry())
1048
- }
1049
-
1050
- // Recursively read through all entries
1051
- for (let entry of entries) {
1052
- const newFiles = await getFilesFromEntry(entry)
1053
- files = files.concat(newFiles)
1054
- }
1055
-
1056
- return files
1057
- }
1058
-
1059
- authenticateWithPuter = function() {
1060
- if(this.env !== 'web'){
1061
- return;
1062
- }
1063
-
1064
- // if authToken is already present, resolve immediately
1065
- if(this.authToken){
1066
- return new Promise((resolve) => {
1067
- resolve();
1068
- })
1069
- }
1070
-
1071
- // If a prompt is already open, return a promise that resolves based on the existing prompt's result.
1072
- if (puter.puterAuthState.isPromptOpen) {
1073
- return new Promise((resolve, reject) => {
1074
- puter.puterAuthState.resolver = { resolve, reject };
1075
- });
1076
- }
1077
-
1078
- // Show the permission prompt and set the state.
1079
- puter.puterAuthState.isPromptOpen = true;
1080
- puter.puterAuthState.authGranted = null;
1081
-
1082
- return new Promise((resolve, reject) => {
1083
- if (!puter.authToken) {
1084
- const puterDialog = new PuterDialog(resolve, reject);
1085
- document.body.appendChild(puterDialog);
1086
- puterDialog.open();
1087
- } else {
1088
- // If authToken is already present, resolve immediately
1089
- resolve();
1090
- }
1091
- });
1092
- }
1093
-
1094
- // Returns a Promise<AppConnection>
1095
- /**
1096
- * launchApp opens the specified app in Puter with the specified argumets.
1097
- * @param {*} nameOrOptions - name of the app as a string, or an options object
1098
- * @param {*} args - named parameters that will be passed to the app as arguments
1099
- * @param {*} callback - in case you don't want to use `await` or `.then()`
1100
- * @returns
1101
- */
1102
- launchApp = async function launchApp(nameOrOptions, args, callback) {
1103
- let pseudonym = undefined;
1104
- let file_paths = undefined;
1105
- let items = undefined;
1106
- let app_name = nameOrOptions; // becomes string after branch below
1107
-
1108
- // Handle case where app_name is an options object
1109
- if (typeof app_name === 'object' && app_name !== null) {
1110
- const options = app_name;
1111
- app_name = options.name || options.app_name;
1112
- file_paths = options.file_paths;
1113
- args = args || options.args;
1114
- callback = callback || options.callback;
1115
- pseudonym = options.pseudonym;
1116
- items = options.items;
1117
- }
1118
-
1119
- if ( items ) {
1120
- if ( ! Array.isArray(items) ) items = [];
1121
- for ( let i=0 ; i < items.length ; i++ ) {
1122
- if ( items[i] instanceof FSItem ) {
1123
- items[i] = items[i]._internalProperties.file_signature;
1124
- }
1125
- }
1126
- }
1127
-
1128
- if ( app_name && app_name.includes('#(as)') ) {
1129
- [app_name, pseudonym] = app_name.split('#(as)');
1130
- }
1131
-
1132
- if ( ! app_name ) app_name = puter.appName;
1133
-
1134
- const app_info = await this.#ipc_stub({
1135
- method: 'launchApp',
1136
- callback,
1137
- parameters: {
1138
- app_name,
1139
- file_paths,
1140
- items,
1141
- pseudonym,
1142
- args,
1143
- },
1144
- });
1145
-
1146
- return AppConnection.from(app_info, this.context);
1147
- }
1148
-
1149
- connectToInstance = async function connectToInstance (app_name) {
1150
- const app_info = await this.#ipc_stub({
1151
- method: 'connectToInstance',
1152
- parameters: {
1153
- app_name,
1154
- }
1155
- });
1156
-
1157
- return AppConnection.from(app_info, this.context);
1158
- }
1159
-
1160
- parentApp() {
1161
- return this.#parentAppConnection;
1162
- }
1163
-
1164
- createWindow = function (options, callback) {
1165
- return new Promise((resolve) => {
1166
- this.#postMessageWithCallback('createWindow', (res)=>{
1167
- resolve(res.window);
1168
- }, { options: options ?? {} });
1169
- })
1170
- }
1171
-
1172
- // Menubar
1173
- menubar = function(){
1174
- // Remove previous style tag
1175
- document.querySelectorAll('style.puter-stylesheet').forEach(function(el) {
1176
- el.remove();
1177
- })
1178
-
1179
- // Add new style tag
1180
- const style = document.createElement('style');
1181
- style.classList.add('puter-stylesheet');
1182
- style.innerHTML = `
1183
- .--puter-menubar {
1184
- border-bottom: 1px solid #e9e9e9;
1185
- background-color: #fbf9f9;
1186
- padding-top: 3px;
1187
- padding-bottom: 2px;
1188
- display: inline-block;
1189
- position: fixed;
1190
- top: 0;
1191
- width: 100%;
1192
- margin: 0;
1193
- padding: 0;
1194
- height: 31px;
1195
- font-family: Arial, Helvetica, sans-serif;
1196
- font-size: 13px;
1197
- z-index: 9999;
1198
- }
1199
-
1200
- .--puter-menubar, .--puter-menubar * {
1201
- user-select: none;
1202
- -webkit-user-select: none;
1203
- cursor: default;
1204
- }
1205
-
1206
- .--puter-menubar .dropdown-item-divider>hr {
1207
- margin-top: 5px;
1208
- margin-bottom: 5px;
1209
- border-bottom: none;
1210
- border-top: 1px solid #00000033;
1211
- }
1212
-
1213
- .--puter-menubar>li {
1214
- display: inline-block;
1215
- padding: 10px 5px;
1216
- }
1217
-
1218
- .--puter-menubar>li>ul {
1219
- display: none;
1220
- z-index: 999999999999;
1221
- list-style: none;
1222
- background-color: rgb(233, 233, 233);
1223
- width: 200px;
1224
- border: 1px solid #e4ebf3de;
1225
- box-shadow: 0px 0px 5px #00000066;
1226
- padding-left: 6px;
1227
- padding-right: 6px;
1228
- padding-top: 4px;
1229
- padding-bottom: 4px;
1230
- color: #333;
1231
- border-radius: 4px;
1232
- padding: 2px;
1233
- min-width: 200px;
1234
- margin-top: 5px;
1235
- position: absolute;
1236
- }
1237
-
1238
- .--puter-menubar .menubar-item {
1239
- display: block;
1240
- line-height: 24px;
1241
- margin-top: -7px;
1242
- text-align: center;
1243
- border-radius: 3px;
1244
- padding: 0 5px;
1245
- }
1246
-
1247
- .--puter-menubar .menubar-item-open {
1248
- background-color: rgb(216, 216, 216);
1249
- }
1250
-
1251
- .--puter-menubar .dropdown-item {
1252
- padding: 5px;
1253
- padding: 5px 30px;
1254
- list-style-type: none;
1255
- user-select: none;
1256
- font-size: 13px;
1257
- }
1258
-
1259
- .--puter-menubar .dropdown-item-icon, .--puter-menubar .dropdown-item-icon-active {
1260
- pointer-events: none;
1261
- width: 18px;
1262
- height: 18px;
1263
- margin-left: -23px;
1264
- margin-bottom: -4px;
1265
- margin-right: 5px;
1266
- }
1267
- .--puter-menubar .dropdown-item-disabled .dropdown-item-icon{
1268
- display: inline-block !important;
1269
- }
1270
- .--puter-menubar .dropdown-item-disabled .dropdown-item-icon-active{
1271
- display: none !important;
1272
- }
1273
- .--puter-menubar .dropdown-item-icon-active {
1274
- display:none;
1275
- }
1276
- .--puter-menubar .dropdown-item:hover .dropdown-item-icon{
1277
- display: none;
1278
- }
1279
- .--puter-menubar .dropdown-item:hover .dropdown-item-icon-active{
1280
- display: inline-block;
1281
- }
1282
- .--puter-menubar .dropdown-item-hide-icon .dropdown-item-icon, .--puter-menubar .dropdown-item-hide-icon .dropdown-item-icon-active{
1283
- display: none !important;
1284
- }
1285
- .--puter-menubar .dropdown-item a {
1286
- color: #333;
1287
- text-decoration: none;
1288
- }
1289
-
1290
- .--puter-menubar .dropdown-item:hover, .--puter-menubar .dropdown-item:hover a {
1291
- background-color: rgb(59 134 226);
1292
- color: white;
1293
- border-radius: 4px;
1294
- }
1295
-
1296
- .--puter-menubar .dropdown-item-disabled, .--puter-menubar .dropdown-item-disabled:hover {
1297
- opacity: 0.5;
1298
- background-color: transparent;
1299
- color: initial;
1300
- cursor: initial;
1301
- pointer-events: none;
1302
- }
1303
-
1304
- .--puter-menubar .menubar * {
1305
- user-select: none;
1306
- }
1307
- `;
1308
- let head = document.head || document.getElementsByTagName('head')[0];
1309
- head.appendChild(style);
1310
-
1311
- document.addEventListener('click', function(e){
1312
- // Don't hide if clicking on disabled item
1313
- if(e.target.classList.contains('dropdown-item-disabled'))
1314
- return false;
1315
- // Hide open menus
1316
- if(!(e.target).classList.contains('menubar-item')){
1317
- document.querySelectorAll('.menubar-item.menubar-item-open').forEach(function(el) {
1318
- el.classList.remove('menubar-item-open');
1319
- })
1320
-
1321
- document.querySelectorAll('.dropdown').forEach(el => el.style.display = "none");
1322
- }
1323
- });
1324
-
1325
- // When focus is gone from this window, hide open menus
1326
- window.addEventListener('blur', function(e){
1327
- document.querySelectorAll('.dropdown').forEach(function(el) {
1328
- el.style.display = "none";
1329
- })
1330
- document.querySelectorAll('.menubar-item.menubar-item-open').forEach(el => el.classList.remove('menubar-item-open'));
1331
- });
1332
-
1333
- // Returns the siblings of the element
1334
- const siblings = function (e) {
1335
- const siblings = [];
1336
-
1337
- // if no parent, return empty list
1338
- if(!e.parentNode) {
1339
- return siblings;
1340
- }
1341
-
1342
- // first child of the parent node
1343
- let sibling = e.parentNode.firstChild;
1344
-
1345
- // get all other siblings
1346
- while (sibling) {
1347
- if (sibling.nodeType === 1 && sibling !== e) {
1348
- siblings.push(sibling);
1349
- }
1350
- sibling = sibling.nextSibling;
1351
- }
1352
- return siblings;
1353
- };
1354
-
1355
- // Open dropdown
1356
- document.querySelectorAll('.menubar-item').forEach(el => el.addEventListener('mousedown', function(e){
1357
- // Hide all other menus
1358
- document.querySelectorAll('.dropdown').forEach(function(el) {
1359
- el.style.display = 'none';
1360
- });
1361
-
1362
- // Remove open class from all menus, except this menu that was just clicked
1363
- document.querySelectorAll('.menubar-item.menubar-item-open').forEach(function(el) {
1364
- if(el != e.target)
1365
- el.classList.remove('menubar-item-open');
1366
- });
1367
-
1368
- // If menu is already open, close it
1369
- if(this.classList.contains('menubar-item-open')){
1370
- document.querySelectorAll('.menubar-item.menubar-item-open').forEach(function(el) {
1371
- el.classList.remove('menubar-item-open');
1372
- });
1373
- }
1374
-
1375
- // If menu is not open, open it
1376
- else if(!e.target.classList.contains('dropdown-item')){
1377
- this.classList.add('menubar-item-open')
1378
-
1379
- // show all sibling
1380
- siblings(this).forEach(function(el) {
1381
- el.style.display = 'block';
1382
- });
1383
- }
1384
-
1385
- }));
1386
-
1387
- // If a menu is open, and you hover over another menu, open that menu
1388
- document.querySelectorAll('.--puter-menubar .menubar-item').forEach(el => el.addEventListener('mouseover', function(e){
1389
- const open_menus = document.querySelectorAll('.menubar-item.menubar-item-open');
1390
- if(open_menus.length > 0 && open_menus[0] !== e.target){
1391
- e.target.dispatchEvent(new Event('mousedown'));
1392
- }
1393
- }))
1394
- }
1395
-
1396
- on(eventName, callback) {
1397
- super.on(eventName, callback);
1398
- // If we already received a broadcast for this event, run the callback immediately
1399
- if (this.#eventNames.includes(eventName) && this.#lastBroadcastValue.has(eventName)) {
1400
- callback(this.#lastBroadcastValue.get(eventName));
1401
- }
1402
- }
1403
-
1404
- #showTime = null;
1405
- #hideTimeout = null;
1406
-
1407
- showSpinner(html) {
1408
- if (this.#overlayActive) return;
1409
-
1410
- // Create and add stylesheet for spinner if it doesn't exist
1411
- if (!document.getElementById('puter-spinner-styles')) {
1412
- const styleSheet = document.createElement('style');
1413
- styleSheet.id = 'puter-spinner-styles';
1414
- styleSheet.textContent = `
1415
- .puter-loading-spinner {
1416
- width: 50px;
1417
- height: 50px;
1418
- border: 5px solid #f3f3f3;
1419
- border-top: 5px solid #3498db;
1420
- border-radius: 50%;
1421
- animation: spin 1s linear infinite;
1422
- margin-bottom: 10px;
1423
- }
1424
-
1425
- .puter-loading-text {
1426
- font-family: Arial, sans-serif;
1427
- font-size: 16px;
1428
- margin-top: 10px;
1429
- text-align: center;
1430
- width: 100%;
1431
- }
1432
-
1433
- @keyframes spin {
1434
- 0% { transform: rotate(0deg); }
1435
- 100% { transform: rotate(360deg); }
1436
- }
1437
-
1438
- .puter-loading-container {
1439
- display: flex;
1440
- flex-direction: column;
1441
- align-items: center;
1442
- justify-content: center;
1443
- min-height: 120px;
1444
- background: #ffffff;
1445
- border-radius: 10px;
1446
- padding: 20px;
1447
- min-width: 120px;
1448
- }
1449
- `;
1450
- document.head.appendChild(styleSheet);
1451
- }
1452
-
1453
- const overlay = document.createElement('div');
1454
- overlay.classList.add('puter-loading-overlay');
1455
-
1456
- const styles = {
1457
- position: 'fixed',
1458
- top: '0',
1459
- left: '0',
1460
- width: '100%',
1461
- height: '100%',
1462
- backgroundColor: 'rgba(255, 255, 255, 0.8)',
1463
- zIndex: '2147483647',
1464
- display: 'flex',
1465
- justifyContent: 'center',
1466
- alignItems: 'center',
1467
- pointerEvents: 'all'
1468
- };
1469
-
1470
- Object.assign(overlay.style, styles);
1471
-
1472
- // Create container for spinner and text
1473
- const container = document.createElement('div');
1474
- container.classList.add('puter-loading-container');
1475
-
1476
- // Add spinner and text
1477
- container.innerHTML = `
1478
- <div class="puter-loading-spinner"></div>
1479
- <div class="puter-loading-text">${html ?? 'Working...'}</div>
1480
- `;
1481
-
1482
- overlay.appendChild(container);
1483
- document.body.appendChild(overlay);
1484
-
1485
- this.#overlayActive = true;
1486
- this.#showTime = Date.now(); // Add show time tracking
1487
- this.#overlayTimer = setTimeout(() => {
1488
- this.#overlayTimer = null;
1489
- }, 1000);
1490
- }
1491
-
1492
- hideSpinner() {
1493
- if (!this.#overlayActive) return;
1494
-
1495
- if (this.#overlayTimer) {
1496
- clearTimeout(this.#overlayTimer);
1497
- this.#overlayTimer = null;
1498
- }
1499
-
1500
- // Calculate how long the spinner has been shown
1501
- const elapsedTime = Date.now() - this.#showTime;
1502
- const remainingTime = Math.max(0, 1200 - elapsedTime);
1503
-
1504
- // If less than 1 second has passed, delay the hide
1505
- if (remainingTime > 0) {
1506
- if (this.#hideTimeout) {
1507
- clearTimeout(this.#hideTimeout);
1508
- }
1509
-
1510
- this.#hideTimeout = setTimeout(() => {
1511
- this.#removeSpinner();
1512
- }, remainingTime);
1513
- } else {
1514
- this.#removeSpinner();
1515
- }
1516
- }
1517
-
1518
- // Add private method to handle spinner removal
1519
- #removeSpinner() {
1520
- const overlay = document.querySelector('.puter-loading-overlay');
1521
- if (overlay) {
1522
- overlay.parentNode?.removeChild(overlay);
1523
- }
1524
-
1525
- this.#overlayActive = false;
1526
- this.#showTime = null;
1527
- this.#hideTimeout = null;
1528
- }
1529
-
1530
- isWorkingActive() {
1531
- return this.#overlayActive;
1532
- }
1533
-
1534
- /**
1535
- * Gets the current language/locale code (e.g., 'en', 'fr', 'es').
1536
- *
1537
- * @returns {Promise<string>} A promise that resolves with the current language code.
1538
- *
1539
- * @example
1540
- * const currentLang = await puter.ui.getLanguage();
1541
- * console.log(`Current language: ${currentLang}`); // e.g., "Current language: fr"
1542
- */
1543
- getLanguage() {
1544
- // In GUI environment, access the global locale directly
1545
- if(this.env === 'gui'){
1546
- return window.locale;
1547
- }
1548
-
1549
- return new Promise((resolve) => {
1550
- this.#postMessageWithCallback('getLanguage', resolve, {});
1551
- });
1552
- }
1553
- }
1554
-
1555
- export default UI