1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033 |
- import FSItem from './FSItem.js';
- import PuterDialog from './PuterDialog.js';
- import EventListener from '../lib/EventListener.js';
- // AppConnection provides an API for interacting with another app.
- // It's returned by UI methods, and cannot be constructed directly by user code.
- // For basic usage:
- // - postMessage(message) Send a message to the target app
- // - on('message', callback) Listen to messages from the target app
- class AppConnection extends EventListener {
- // targetOrigin for postMessage() calls to Puter
- #puterOrigin = '*';
- // Whether the target app is open
- #isOpen;
- // Whether the target app uses the Puter SDK, and so accepts messages
- // (Closing and close events will still function.)
- #usesSDK;
- constructor(messageTarget, appInstanceID, targetAppInstanceID, usesSDK) {
- super([
- 'message', // The target sent us something with postMessage()
- 'close', // The target app was closed
- ]);
- this.messageTarget = messageTarget;
- this.appInstanceID = appInstanceID;
- this.targetAppInstanceID = targetAppInstanceID;
- this.#isOpen = true;
- this.#usesSDK = usesSDK;
- // TODO: Set this.#puterOrigin to the puter origin
- window.addEventListener('message', event => {
- if (event.data.msg === 'messageToApp') {
- if (event.data.appInstanceID !== this.targetAppInstanceID) {
- // Message is from a different AppConnection; ignore it.
- return;
- }
- if (event.data.targetAppInstanceID !== this.appInstanceID) {
- console.error(`AppConnection received message intended for wrong app! appInstanceID=${this.appInstanceID}, target=${event.data.targetAppInstanceID}`);
- return;
- }
- this.emit('message', event.data.contents);
- return;
- }
- if (event.data.msg === 'appClosed') {
- if (event.data.appInstanceID !== this.targetAppInstanceID) {
- // Message is from a different AppConnection; ignore it.
- return;
- }
- this.#isOpen = false;
- this.emit('close', {
- appInstanceID: this.targetAppInstanceID,
- });
- }
- });
- }
- // Does the target app use the Puter SDK? If not, certain features will be unavailable.
- get usesSDK() { return this.#usesSDK; }
- // Send a message to the target app. Requires the target to use the Puter SDK.
- postMessage(message) {
- if (!this.#isOpen) {
- console.warn('Trying to post message on a closed AppConnection');
- return;
- }
- if (!this.#usesSDK) {
- console.warn('Trying to post message to a non-SDK app');
- return;
- }
- this.messageTarget.postMessage({
- msg: 'messageToApp',
- appInstanceID: this.appInstanceID,
- targetAppInstanceID: this.targetAppInstanceID,
- targetAppOrigin: '*', // TODO: Specify this somehow
- contents: message,
- }, this.#puterOrigin);
- }
- // Attempt to close the target application
- close() {
- if (!this.#isOpen) {
- console.warn('Trying to close an app on a closed AppConnection');
- return;
- }
- this.messageTarget.postMessage({
- msg: 'closeApp',
- appInstanceID: this.appInstanceID,
- targetAppInstanceID: this.targetAppInstanceID,
- }, this.#puterOrigin);
- }
- }
- class UI extends EventListener {
- // Used to generate a unique message id for each message sent to the host environment
- // we start from 1 because 0 is falsy and we want to avoid that for the message id
- #messageID = 1;
- // Holds the callback functions for the various events
- // that are triggered when a watched item has changed.
- itemWatchCallbackFunctions = [];
- // Holds the unique app instance ID that is provided by the host environment
- appInstanceID;
- // Holds the unique app instance ID for the parent (if any), which is provided by the host environment
- parentInstanceID;
- // If we have a parent app, holds an AppConnection to it
- #parentAppConnection = null;
- // Holds the callback functions for the various events
- // that can be triggered by the host environment's messages.
- #callbackFunctions = [];
- // onWindowClose() is executed right before the window is closed. Users can override this function
- // to perform a variety of tasks right before window is closed. Users can override this function.
- #onWindowClose;
- // When an item is opened by this app in any way onItemsOpened() is executed. Users can override this function.
- #onItemsOpened;
- #onLaunchedWithItems;
- // List of events that can be listened to.
- #eventNames;
- // The most recent value that we received for a given broadcast, by name.
- #lastBroadcastValue = new Map(); // name -> data
- // Replaces boilerplate for most methods: posts a message to the GUI with a unique ID, and sets a callback for it.
- #postMessageWithCallback = function(name, resolve, args = {}) {
- const msg_id = this.#messageID++;
- this.messageTarget?.postMessage({
- msg: name,
- env: this.env,
- appInstanceID: this.appInstanceID,
- uuid: msg_id,
- ...args,
- }, '*');
- //register callback
- this.#callbackFunctions[msg_id] = resolve;
- }
- constructor (appInstanceID, parentInstanceID, appID, env) {
- const eventNames = [
- 'localeChanged',
- 'themeChanged',
- ];
- super(eventNames);
- this.#eventNames = eventNames;
- this.appInstanceID = appInstanceID;
- this.parentInstanceID = parentInstanceID;
- this.appID = appID;
- this.env = env;
- if(this.env === 'app'){
- this.messageTarget = window.parent;
- }
- else if(this.env === 'gui'){
- return;
- }
- if (this.parentInstanceID) {
- this.#parentAppConnection = new AppConnection(this.messageTarget, this.appInstanceID, this.parentInstanceID, true);
- }
- // Tell the host environment that this app is using the Puter SDK and is ready to receive messages,
- // this will allow the OS to send custom messages to the app
- this.messageTarget?.postMessage({
- msg: "READY",
- appInstanceID: this.appInstanceID,
- }, '*');
- // When this app's window is focused send a message to the host environment
- window.addEventListener('focus', (e) => {
- this.messageTarget?.postMessage({
- msg: "windowFocused",
- appInstanceID: this.appInstanceID,
- }, '*');
- });
- // Bind the message event listener to the window
- let lastDraggedOverElement = null;
- window.addEventListener('message', async (e) => {
- // `error`
- if(e.data.error){
- throw e.data.error;
- }
- // `focus` event
- else if(e.data.msg && e.data.msg === 'focus'){
- window.focus();
- }
- // `click` event
- else if(e.data.msg && e.data.msg === 'click'){
- // Get the element that was clicked on and click it
- const clicked_el = document.elementFromPoint(e.data.x, e.data.y);
- if(clicked_el !== null)
- clicked_el.click();
- }
- // `dragover` event based on the `drag` event from the host environment
- else if(e.data.msg && e.data.msg === 'drag'){
- // Get the element being dragged over
- const draggedOverElement = document.elementFromPoint(e.data.x, e.data.y);
- if(draggedOverElement !== lastDraggedOverElement){
- // If the last element exists and is different from the current, dispatch a dragleave on it
- if(lastDraggedOverElement){
- const dragLeaveEvent = new Event('dragleave', {
- bubbles: true,
- cancelable: true,
- clientX: e.data.x,
- clientY: e.data.y
- });
- lastDraggedOverElement.dispatchEvent(dragLeaveEvent);
- }
- // If the current element exists and is different from the last, dispatch dragenter on it
- if(draggedOverElement){
- const dragEnterEvent = new Event('dragenter', {
- bubbles: true,
- cancelable: true,
- clientX: e.data.x,
- clientY: e.data.y
- });
- draggedOverElement.dispatchEvent(dragEnterEvent);
- }
- // Update the lastDraggedOverElement
- lastDraggedOverElement = draggedOverElement;
- }
- }
- // `drop` event
- else if(e.data.msg && e.data.msg === 'drop'){
- if(lastDraggedOverElement){
- const dropEvent = new CustomEvent('drop', {
- bubbles: true,
- cancelable: true,
- detail: {
- clientX: e.data.x,
- clientY: e.data.y,
- items: e.data.items
- }
- });
- lastDraggedOverElement.dispatchEvent(dropEvent);
-
- // Reset the lastDraggedOverElement
- lastDraggedOverElement = null;
- }
- }
- // windowWillClose
- else if(e.data.msg === 'windowWillClose'){
- // If the user has not overridden onWindowClose() then send a message back to the host environment
- // to let it know that it is ok to close the window.
- if(this.#onWindowClose === undefined){
- this.messageTarget?.postMessage({
- msg: true,
- appInstanceID: this.appInstanceID,
- original_msg_id: e.data.msg_id,
- }, '*');
- }
- // If the user has overridden onWindowClose() then send a message back to the host environment
- // to let it know that it is NOT ok to close the window. Then execute onWindowClose() and the user will
- // have to manually close the window.
- else{
- this.messageTarget?.postMessage({
- msg: false,
- appInstanceID: this.appInstanceID,
- original_msg_id: e.data.msg_id,
- }, '*');
- this.#onWindowClose();
- }
- }
- // itemsOpened
- else if(e.data.msg === 'itemsOpened'){
- // If the user has not overridden onItemsOpened() then only send a message back to the host environment
- if(this.#onItemsOpened === undefined){
- this.messageTarget?.postMessage({
- msg: true,
- appInstanceID: this.appInstanceID,
- original_msg_id: e.data.msg_id,
- }, '*');
- }
- // If the user has overridden onItemsOpened() then send a message back to the host environment
- // and execute onItemsOpened()
- else{
- this.messageTarget?.postMessage({
- msg: false,
- appInstanceID: this.appInstanceID,
- original_msg_id: e.data.msg_id,
- }, '*');
- let items = [];
- if(e.data.items.length > 0){
- for (let index = 0; index < e.data.items.length; index++)
- items.push(new FSItem(e.data.items[index]))
- }
- this.#onItemsOpened(items);
- }
- }
- // getAppDataSucceeded
- else if(e.data.msg === 'getAppDataSucceeded'){
- let appDataItem = new FSItem(e.data.item);
- if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
- this.#callbackFunctions[e.data.original_msg_id](appDataItem);
- }
- }
- // readAppDataFileSucceeded
- else if(e.data.msg === 'readAppDataFileSucceeded'){
- let appDataItem = new FSItem(e.data.item);
- if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
- this.#callbackFunctions[e.data.original_msg_id](appDataItem);
- }
- }
- // readAppDataFileFailed
- else if(e.data.msg === 'readAppDataFileFailed'){
- if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
- this.#callbackFunctions[e.data.original_msg_id](null);
- }
- }
- // Determine if this is a response to a previous message and if so, is there
- // a callback function for this message? if answer is yes to both then execute the callback
- else if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
- if(e.data.msg === 'fileOpenPicked'){
- // 1 item returned
- if(e.data.items.length === 1){
- this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.items[0]));
- }
- // multiple items returned
- else if(e.data.items.length > 1){
- // multiple items returned
- let items = [];
- for (let index = 0; index < e.data.items.length; index++)
- items.push(new FSItem(e.data.items[index]))
- this.#callbackFunctions[e.data.original_msg_id](items);
- }
- }
- else if(e.data.msg === 'directoryPicked'){
- // 1 item returned
- if(e.data.items.length === 1){
- this.#callbackFunctions[e.data.original_msg_id](new FSItem({
- uid: e.data.items[0].uid,
- name: e.data.items[0].fsentry_name,
- path: e.data.items[0].path,
- readURL: e.data.items[0].read_url,
- writeURL: e.data.items[0].write_url,
- metadataURL: e.data.items[0].metadata_url,
- isDirectory: true,
- size: e.data.items[0].fsentry_size,
- accessed: e.data.items[0].fsentry_accessed,
- modified: e.data.items[0].fsentry_modified,
- created: e.data.items[0].fsentry_created,
- }));
- }
- // multiple items returned
- else if(e.data.items.length > 1){
- // multiple items returned
- let items = [];
- for (let index = 0; index < e.data.items.length; index++)
- items.push(new FSItem(e.data.items[index]))
- this.#callbackFunctions[e.data.original_msg_id](items);
- }
- }
- else if(e.data.msg === 'colorPicked'){
- // execute callback
- this.#callbackFunctions[e.data.original_msg_id](e.data.color);
- }
- else if(e.data.msg === 'fontPicked'){
- // execute callback
- this.#callbackFunctions[e.data.original_msg_id](e.data.font);
- }
- else if(e.data.msg === 'alertResponded'){
- // execute callback
- this.#callbackFunctions[e.data.original_msg_id](e.data.response);
- }
- else if(e.data.msg === 'promptResponded'){
- // execute callback
- this.#callbackFunctions[e.data.original_msg_id](e.data.response);
- }
- else if(e.data.msg === "fileSaved"){
- // execute callback
- this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.saved_file));
- }
- else if (e.data.msg === 'childAppLaunched') {
- // execute callback with a new AppConnection to the child
- const connection = new AppConnection(this.messageTarget, this.appInstanceID, e.data.child_instance_id, e.data.uses_sdk);
- this.#callbackFunctions[e.data.original_msg_id](connection);
- }
- else{
- // execute callback
- this.#callbackFunctions[e.data.original_msg_id](e.data);
- }
- //remove this callback function since it won't be needed again
- delete this.#callbackFunctions[e.data.original_msg_id];
- }
- // Item Watch response
- else if(e.data.msg === "itemChanged" && e.data.data && e.data.data.uid){
- //excute callback
- if(itemWatchCallbackFunctions[e.data.data.uid] && typeof itemWatchCallbackFunctions[e.data.data.uid] === 'function')
- itemWatchCallbackFunctions[e.data.data.uid](e.data.data);
- }
- // Broadcasts
- else if (e.data.msg === 'broadcast') {
- const { name, data } = e.data;
- if (!this.#eventNames.includes(name)) {
- return;
- }
- this.emit(name, data);
- this.#lastBroadcastValue.set(name, data);
- }
- });
- }
- onWindowClose = function(callback) {
- this.#onWindowClose = callback;
- }
- onItemsOpened = function(callback) {
- // DEPRECATED - this is also called when items are dropped on the app, which in new versions should be handled
- // with the 'drop' event.
- // Check if a file was opened with this app, i.e. check URL parameters of window/iframe
- // Even though the file has been opened when the app is launched, we need to wait for the onItemsOpened callback to be set
- // before we can call it. This is why we need to check the URL parameters here.
- // This should also be done only the very first time the callback is set (hence the if(!this.#onItemsOpened) check) since
- // the URL parameters will be checked every time the callback is set which can cause problems if the callback is set multiple times.
- if(!this.#onItemsOpened){
- let URLParams = new URLSearchParams(window.location.search);
- if(URLParams.has('puter.item.name') && URLParams.has('puter.item.uid') && URLParams.has('puter.item.read_url')){
- let fpath = URLParams.get('puter.item.path');
- fpath = `~/` + fpath.split('/').slice(2).join('/');
- callback([new FSItem({
- name: URLParams.get('puter.item.name'),
- path: fpath,
- uid: URLParams.get('puter.item.uid'),
- readURL: URLParams.get('puter.item.read_url'),
- writeURL: URLParams.get('puter.item.write_url'),
- metadataURL: URLParams.get('puter.item.metadata_url'),
- size: URLParams.get('puter.item.size'),
- accessed: URLParams.get('puter.item.accessed'),
- modified: URLParams.get('puter.item.modified'),
- created: URLParams.get('puter.item.created'),
- })]);
- }
- }
- this.#onItemsOpened = callback;
- }
- onLaunchedWithItems = function(callback) {
- // Check if a file was opened with this app, i.e. check URL parameters of window/iframe
- // Even though the file has been opened when the app is launched, we need to wait for the onLaunchedWithItems callback to be set
- // before we can call it. This is why we need to check the URL parameters here.
- // This should also be done only the very first time the callback is set (hence the if(!this.#onLaunchedWithItems) check) since
- // the URL parameters will be checked every time the callback is set which can cause problems if the callback is set multiple times.
- if(!this.#onLaunchedWithItems){
- let URLParams = new URLSearchParams(window.location.search);
- if(URLParams.has('puter.item.name') && URLParams.has('puter.item.uid') && URLParams.has('puter.item.read_url')){
- let fpath = URLParams.get('puter.item.path');
- fpath = `~/` + fpath.split('/').slice(2).join('/');
- callback([new FSItem({
- name: URLParams.get('puter.item.name'),
- path: fpath,
- uid: URLParams.get('puter.item.uid'),
- readURL: URLParams.get('puter.item.read_url'),
- writeURL: URLParams.get('puter.item.write_url'),
- metadataURL: URLParams.get('puter.item.metadata_url'),
- size: URLParams.get('puter.item.size'),
- accessed: URLParams.get('puter.item.accessed'),
- modified: URLParams.get('puter.item.modified'),
- created: URLParams.get('puter.item.created'),
- })]);
- }
- }
- this.#onLaunchedWithItems = callback;
- }
- alert = function(message, buttons, options, callback) {
- return new Promise((resolve) => {
- this.#postMessageWithCallback('ALERT', resolve, { message, buttons, options });
- })
- }
- prompt = function(message, placeholder, options, callback) {
- return new Promise((resolve) => {
- this.#postMessageWithCallback('PROMPT', resolve, { message, placeholder, options });
- })
- }
- showDirectoryPicker = function(options, callback){
- return new Promise((resolve) => {
- const msg_id = this.#messageID++;
- if(this.env === 'app'){
- this.messageTarget?.postMessage({
- msg: "showDirectoryPicker",
- appInstanceID: this.appInstanceID,
- uuid: msg_id,
- options: options,
- env: this.env,
- }, '*');
- }else{
- let w = 700;
- let h = 400;
- let title = 'Puter: Open Directory';
- var left = (screen.width/2)-(w/2);
- var top = (screen.height/2)-(h/2);
- 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)}`,
- title,
- 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
- }
- //register callback
- this.#callbackFunctions[msg_id] = resolve;
- })
- }
- showOpenFilePicker = function(options, callback){
- return new Promise((resolve) => {
- const msg_id = this.#messageID++;
- if(this.env === 'app'){
- this.messageTarget?.postMessage({
- msg: "showOpenFilePicker",
- appInstanceID: this.appInstanceID,
- uuid: msg_id,
- options: options ?? {},
- env: this.env,
- }, '*');
- }else{
- let w = 700;
- let h = 400;
- let title = 'Puter: Open File';
- var left = (screen.width/2)-(w/2);
- var top = (screen.height/2)-(h/2);
- 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 ?? {})}`,
- title,
- 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
- }
- //register callback
- this.#callbackFunctions[msg_id] = resolve;
- })
- }
- showFontPicker = function(options){
- return new Promise((resolve) => {
- this.#postMessageWithCallback('showFontPicker', resolve, { options: options ?? {} });
- })
- }
- showColorPicker = function(options){
- return new Promise((resolve) => {
- this.#postMessageWithCallback('showColorPicker', resolve, { options: options ?? {} });
- })
- }
- showSaveFilePicker = function(content, suggestedName){
- return new Promise((resolve) => {
- const msg_id = this.#messageID++;
- const url = (Object.prototype.toString.call(content) === '[object URL]' ? content : undefined);
- if(this.env === 'app'){
- this.messageTarget?.postMessage({
- msg: "showSaveFilePicker",
- appInstanceID: this.appInstanceID,
- content: url ? undefined : content,
- url: url ? url.toString() : undefined,
- suggestedName: suggestedName ?? '',
- env: this.env,
- uuid: msg_id
- }, '*');
- }else{
- window.addEventListener('message', async (e) => {
- if(e.data?.msg === "sendMeFileData"){
- // Send the blob URL to the host environment
- e.source.postMessage({
- msg: "showSaveFilePickerPopup",
- content: url ? undefined : content,
- url: url ? url.toString() : undefined,
- suggestedName: suggestedName ?? '',
- env: this.env,
- uuid: msg_id
- }, '*');
- // remove the event listener
- window.removeEventListener('message', this);
- }
- });
- // Create a Blob from your binary data
- let blob = new Blob([content], {type: 'application/octet-stream'});
- // Create an object URL for the Blob
- let objectUrl = URL.createObjectURL(blob);
- let w = 700;
- let h = 400;
- let title = 'Puter: Save File';
- var left = (screen.width/2)-(w/2);
- var top = (screen.height/2)-(h/2);
- 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)}`,
- title,
- 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
- }
- //register callback
- this.#callbackFunctions[msg_id] = resolve;
- })
- }
- setWindowTitle = function(title, callback) {
- return new Promise((resolve) => {
- this.#postMessageWithCallback('setWindowTitle', resolve, { new_title: title });
- })
- }
- setWindowWidth = function(width, callback) {
- return new Promise((resolve) => {
- this.#postMessageWithCallback('setWindowWidth', resolve, { width });
- })
- }
- setWindowHeight = function(height, callback) {
- return new Promise((resolve) => {
- this.#postMessageWithCallback('setWindowHeight', resolve, { height });
- })
- }
- setWindowSize = function(width, height, callback) {
- return new Promise((resolve) => {
- this.#postMessageWithCallback('setWindowSize', resolve, { width, height });
- })
- }
- setWindowPosition = function(x, y, callback) {
- return new Promise((resolve) => {
- this.#postMessageWithCallback('setWindowPosition', resolve, { x, y });
- })
- }
- /**
- * Asynchronously extracts entries from DataTransferItems, like files and directories.
- *
- * @private
- * @function
- * @async
- * @param {DataTransferItemList} dataTransferItems - List of data transfer items from a drag-and-drop operation.
- * @param {Object} [options={}] - Optional settings.
- * @param {boolean} [options.raw=false] - Determines if the file path should be processed.
- * @returns {Promise<Array<File|Entry>>} - A promise that resolves to an array of File or Entry objects.
- * @throws {Error} - Throws an error if there's an EncodingError and provides information about how to solve it.
- *
- * @example
- * const items = event.dataTransfer.items;
- * const entries = await getEntriesFromDataTransferItems(items, { raw: false });
- */
- getEntriesFromDataTransferItems = async function(dataTransferItems, options = { raw: false }) {
- const checkErr = (err) => {
- if (this.getEntriesFromDataTransferItems.didShowInfo) return
- if (err.name !== 'EncodingError') return
- this.getEntriesFromDataTransferItems.didShowInfo = true
- const infoMsg = `${err.name} occured within datatransfer-files-promise module\n`
- + `Error message: "${err.message}"\n`
- + 'Try serving html over http if currently you are running it from the filesystem.'
- console.warn(infoMsg)
- }
- const readFile = (entry, path = '') => {
- return new Promise((resolve, reject) => {
- entry.file(file => {
- if (!options.raw) file.filepath = path + file.name // save full path
- resolve(file)
- }, (err) => {
- checkErr(err)
- reject(err)
- })
- })
- }
- const dirReadEntries = (dirReader, path) => {
- return new Promise((resolve, reject) => {
- dirReader.readEntries(async entries => {
- let files = []
- for (let entry of entries) {
- const itemFiles = await getFilesFromEntry(entry, path)
- files = files.concat(itemFiles)
- }
- resolve(files)
- }, (err) => {
- checkErr(err)
- reject(err)
- })
- })
- }
- const readDir = async (entry, path) => {
- const dirReader = entry.createReader()
- const newPath = path + entry.name + '/'
- let files = []
- let newFiles
- do {
- newFiles = await dirReadEntries(dirReader, newPath)
- files = files.concat(newFiles)
- } while (newFiles.length > 0)
- return files
- }
- const getFilesFromEntry = async (entry, path = '') => {
- if(entry === null)
- return;
- else if (entry.isFile) {
- const file = await readFile(entry, path)
- return [file]
- }
- else if (entry.isDirectory) {
- const files = await readDir(entry, path)
- files.push(entry)
- return files
- }
- }
- let files = []
- let entries = []
- // Pull out all entries before reading them
- for (let i = 0, ii = dataTransferItems.length; i < ii; i++) {
- entries.push(dataTransferItems[i].webkitGetAsEntry())
- }
- // Recursively read through all entries
- for (let entry of entries) {
- const newFiles = await getFilesFromEntry(entry)
- files = files.concat(newFiles)
- }
- return files
- }
- authenticateWithPuter = function() {
- if(this.env !== 'web'){
- return;
- }
- // if authToken is already present, resolve immediately
- if(this.authToken){
- return new Promise((resolve) => {
- resolve();
- })
- }
- // If a prompt is already open, return a promise that resolves based on the existing prompt's result.
- if (puter.puterAuthState.isPromptOpen) {
- return new Promise((resolve, reject) => {
- puter.puterAuthState.resolver = { resolve, reject };
- });
- }
- // Show the permission prompt and set the state.
- puter.puterAuthState.isPromptOpen = true;
- puter.puterAuthState.authGranted = null;
- return new Promise((resolve, reject) => {
- if (!puter.authToken) {
- const puterDialog = new PuterDialog(resolve, reject);
- document.body.appendChild(puterDialog);
- puterDialog.open();
- } else {
- // If authToken is already present, resolve immediately
- resolve();
- }
- });
- }
- // Returns a Promise<AppConnection>
- launchApp = function(appName, args, callback) {
- return new Promise((resolve) => {
- // if appName is an object and args is not set, then appName is actually args
- if (typeof appName === 'object' && !args) {
- args = appName;
- appName = undefined;
- }
- this.#postMessageWithCallback('launchApp', resolve, { app_name: appName, args });
- })
- }
- parentApp() {
- return this.#parentAppConnection;
- }
- createWindow = function (options, callback) {
- return new Promise((resolve) => {
- this.#postMessageWithCallback('createWindow', resolve, { options: options ?? {} });
- })
- }
- // Menubar
- menubar = function(){
- // Remove previous style tag
- document.querySelectorAll('style.puter-stylesheet').forEach(function(el) {
- el.remove();
- })
- // Add new style tag
- const style = document.createElement('style');
- style.classList.add('puter-stylesheet');
- style.innerHTML = `
- .--puter-menubar {
- border-bottom: 1px solid #e9e9e9;
- background-color: #fbf9f9;
- padding-top: 3px;
- padding-bottom: 2px;
- display: inline-block;
- position: fixed;
- top: 0;
- width: 100%;
- margin: 0;
- padding: 0;
- height: 31px;
- font-family: Arial, Helvetica, sans-serif;
- font-size: 13px;
- z-index: 9999;
- }
-
- .--puter-menubar, .--puter-menubar * {
- user-select: none;
- -webkit-user-select: none;
- cursor: default;
- }
-
- .--puter-menubar .dropdown-item-divider>hr {
- margin-top: 5px;
- margin-bottom: 5px;
- border-bottom: none;
- border-top: 1px solid #00000033;
- }
-
- .--puter-menubar>li {
- display: inline-block;
- padding: 10px 5px;
- }
-
- .--puter-menubar>li>ul {
- display: none;
- z-index: 999999999999;
- list-style: none;
- background-color: rgb(233, 233, 233);
- width: 200px;
- border: 1px solid #e4ebf3de;
- box-shadow: 0px 0px 5px #00000066;
- padding-left: 6px;
- padding-right: 6px;
- padding-top: 4px;
- padding-bottom: 4px;
- color: #333;
- border-radius: 4px;
- padding: 2px;
- min-width: 200px;
- margin-top: 5px;
- position: absolute;
- }
-
- .--puter-menubar .menubar-item {
- display: block;
- line-height: 24px;
- margin-top: -7px;
- text-align: center;
- border-radius: 3px;
- padding: 0 5px;
- }
-
- .--puter-menubar .menubar-item-open {
- background-color: rgb(216, 216, 216);
- }
-
- .--puter-menubar .dropdown-item {
- padding: 5px;
- padding: 5px 30px;
- list-style-type: none;
- user-select: none;
- font-size: 13px;
- }
-
- .--puter-menubar .dropdown-item-icon, .--puter-menubar .dropdown-item-icon-active {
- pointer-events: none;
- width: 18px;
- height: 18px;
- margin-left: -23px;
- margin-bottom: -4px;
- margin-right: 5px;
- }
- .--puter-menubar .dropdown-item-disabled .dropdown-item-icon{
- display: inline-block !important;
- }
- .--puter-menubar .dropdown-item-disabled .dropdown-item-icon-active{
- display: none !important;
- }
- .--puter-menubar .dropdown-item-icon-active {
- display:none;
- }
- .--puter-menubar .dropdown-item:hover .dropdown-item-icon{
- display: none;
- }
- .--puter-menubar .dropdown-item:hover .dropdown-item-icon-active{
- display: inline-block;
- }
- .--puter-menubar .dropdown-item-hide-icon .dropdown-item-icon, .--puter-menubar .dropdown-item-hide-icon .dropdown-item-icon-active{
- display: none !important;
- }
- .--puter-menubar .dropdown-item a {
- color: #333;
- text-decoration: none;
- }
-
- .--puter-menubar .dropdown-item:hover, .--puter-menubar .dropdown-item:hover a {
- background-color: rgb(59 134 226);
- color: white;
- border-radius: 4px;
- }
-
- .--puter-menubar .dropdown-item-disabled, .--puter-menubar .dropdown-item-disabled:hover {
- opacity: 0.5;
- background-color: transparent;
- color: initial;
- cursor: initial;
- pointer-events: none;
- }
-
- .--puter-menubar .menubar * {
- user-select: none;
- }
- `;
- let head = document.head || document.getElementsByTagName('head')[0];
- head.appendChild(style);
- document.addEventListener('click', function(e){
- // Don't hide if clicking on disabled item
- if(e.target.classList.contains('dropdown-item-disabled'))
- return false;
- // Hide open menus
- if(!(e.target).classList.contains('menubar-item')){
- document.querySelectorAll('.menubar-item.menubar-item-open').forEach(function(el) {
- el.classList.remove('menubar-item-open');
- })
- document.querySelectorAll('.dropdown').forEach(el => el.style.display = "none");
- }
- });
- // When focus is gone from this window, hide open menus
- window.addEventListener('blur', function(e){
- document.querySelectorAll('.dropdown').forEach(function(el) {
- el.style.display = "none";
- })
- document.querySelectorAll('.menubar-item.menubar-item-open').forEach(el => el.classList.remove('menubar-item-open'));
- });
- // Returns the siblings of the element
- const siblings = function (e) {
- const siblings = [];
- // if no parent, return empty list
- if(!e.parentNode) {
- return siblings;
- }
- // first child of the parent node
- let sibling = e.parentNode.firstChild;
- // get all other siblings
- while (sibling) {
- if (sibling.nodeType === 1 && sibling !== e) {
- siblings.push(sibling);
- }
- sibling = sibling.nextSibling;
- }
- return siblings;
- };
- // Open dropdown
- document.querySelectorAll('.menubar-item').forEach(el => el.addEventListener('mousedown', function(e){
- // Hide all other menus
- document.querySelectorAll('.dropdown').forEach(function(el) {
- el.style.display = 'none';
- });
-
- // Remove open class from all menus, except this menu that was just clicked
- document.querySelectorAll('.menubar-item.menubar-item-open').forEach(function(el) {
- if(el != e.target)
- el.classList.remove('menubar-item-open');
- });
-
- // If menu is already open, close it
- if(this.classList.contains('menubar-item-open')){
- document.querySelectorAll('.menubar-item.menubar-item-open').forEach(function(el) {
- el.classList.remove('menubar-item-open');
- });
- }
- // If menu is not open, open it
- else if(!e.target.classList.contains('dropdown-item')){
- this.classList.add('menubar-item-open')
-
- // show all sibling
- siblings(this).forEach(function(el) {
- el.style.display = 'block';
- });
- }
- }));
- // If a menu is open, and you hover over another menu, open that menu
- document.querySelectorAll('.--puter-menubar .menubar-item').forEach(el => el.addEventListener('mouseover', function(e){
- const open_menus = document.querySelectorAll('.menubar-item.menubar-item-open');
- if(open_menus.length > 0 && open_menus[0] !== e.target){
- e.target.dispatchEvent(new Event('mousedown'));
- }
- }))
- }
- on(eventName, callback) {
- super.on(eventName, callback);
- // If we already received a broadcast for this event, run the callback immediately
- if (this.#eventNames.includes(eventName) && this.#lastBroadcastValue.has(eventName)) {
- callback(this.#lastBroadcastValue.get(eventName));
- }
- }
- }
- export default UI
|