@heyputer/puter.js 2.0.1 → 2.0.2
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/APACHE_LICENSE.txt +201 -0
- package/doc/devlog.md +49 -0
- package/index.d.ts +6 -6
- package/package.json +2 -6
- package/src/bg.png +0 -0
- package/src/bg.webp +0 -0
- package/src/entry.js +9 -0
- package/src/lib/APICallLogger.js +110 -0
- package/src/lib/EventListener.js +51 -0
- package/src/lib/RequestError.js +6 -0
- package/src/lib/filesystem/APIFS.js +73 -0
- package/src/lib/filesystem/CacheFS.js +243 -0
- package/src/lib/filesystem/PostMessageFS.js +40 -0
- package/src/lib/filesystem/definitions.js +39 -0
- package/src/lib/path.js +509 -0
- package/src/lib/polyfills/localStorage.js +92 -0
- package/src/lib/polyfills/xhrshim.js +233 -0
- package/src/lib/socket.io/socket.io.esm.min.js +7 -0
- package/src/lib/socket.io/socket.io.esm.min.js.map +1 -0
- package/src/lib/socket.io/socket.io.js +4385 -0
- package/src/lib/socket.io/socket.io.js.map +1 -0
- package/src/lib/socket.io/socket.io.min.js +7 -0
- package/src/lib/socket.io/socket.io.min.js.map +1 -0
- package/src/lib/socket.io/socket.io.msgpack.min.js +7 -0
- package/src/lib/socket.io/socket.io.msgpack.min.js.map +1 -0
- package/src/lib/utils.js +620 -0
- package/src/lib/xdrpc.js +104 -0
- package/src/modules/AI.js +680 -0
- package/src/modules/Apps.js +215 -0
- package/src/modules/Auth.js +171 -0
- package/src/modules/Debug.js +39 -0
- package/src/modules/Drivers.js +278 -0
- package/src/modules/FSItem.js +139 -0
- package/src/modules/FileSystem/index.js +187 -0
- package/src/modules/FileSystem/operations/copy.js +64 -0
- package/src/modules/FileSystem/operations/deleteFSEntry.js +59 -0
- package/src/modules/FileSystem/operations/getReadUrl.js +42 -0
- package/src/modules/FileSystem/operations/mkdir.js +62 -0
- package/src/modules/FileSystem/operations/move.js +75 -0
- package/src/modules/FileSystem/operations/read.js +46 -0
- package/src/modules/FileSystem/operations/readdir.js +102 -0
- package/src/modules/FileSystem/operations/rename.js +58 -0
- package/src/modules/FileSystem/operations/sign.js +103 -0
- package/src/modules/FileSystem/operations/space.js +40 -0
- package/src/modules/FileSystem/operations/stat.js +95 -0
- package/src/modules/FileSystem/operations/symlink.js +55 -0
- package/src/modules/FileSystem/operations/upload.js +440 -0
- package/src/modules/FileSystem/operations/write.js +65 -0
- package/src/modules/FileSystem/utils/getAbsolutePathForApp.js +21 -0
- package/src/modules/Hosting.js +138 -0
- package/src/modules/KV.js +301 -0
- package/src/modules/OS.js +95 -0
- package/src/modules/Perms.js +109 -0
- package/src/modules/PuterDialog.js +481 -0
- package/src/modules/Threads.js +75 -0
- package/src/modules/UI.js +1555 -0
- package/src/modules/Util.js +38 -0
- package/src/modules/Workers.js +120 -0
- package/src/modules/networking/PSocket.js +87 -0
- package/src/modules/networking/PTLS.js +100 -0
- package/src/modules/networking/PWispHandler.js +89 -0
- package/src/modules/networking/parsers.js +157 -0
- package/src/modules/networking/requests.js +282 -0
- package/src/services/APIAccess.js +46 -0
- package/src/services/FSRelay.js +20 -0
- package/src/services/Filesystem.js +122 -0
- package/src/services/NoPuterYet.js +20 -0
- package/src/services/XDIncoming.js +44 -0
- package/test/ai.test.js +214 -0
- package/test/fs.test.js +798 -0
- package/test/index.html +1183 -0
- package/test/kv.test.js +548 -0
- package/test/txt2speech.test.js +178 -0
- package/webpack.config.js +25 -0
|
@@ -0,0 +1,1555 @@
|
|
|
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
|