Browse Source

Refactor `launch_app` to add support for fullpage apps on landing

Nariman Jelveh 11 months ago
parent
commit
b789bb7078
11 changed files with 367 additions and 316 deletions
  1. 2 1
      src/IPC.js
  2. 5 3
      src/UI/UIDesktop.js
  3. 4 3
      src/UI/UIItem.js
  4. 5 4
      src/UI/UITaskbar.js
  5. 4 3
      src/UI/UITaskbarItem.js
  6. 4 3
      src/UI/UIWindow.js
  7. 2 0
      src/globals.js
  8. 4 298
      src/helpers.js
  9. 327 0
      src/helpers/launch_app.js
  10. 8 0
      src/initgui.js
  11. 2 1
      src/keyboard.js

+ 2 - 1
src/IPC.js

@@ -29,6 +29,7 @@ import download from './helpers/download.js';
 import path from "./lib/path.js";
 import UIContextMenu from './UI/UIContextMenu.js';
 import update_mouse_position from './helpers/update_mouse_position.js';
+import launch_app from './helpers/launch_app.js';
 
 /**
  * In Puter, apps are loaded in iframes and communicate with the graphical user interface (GUI), and each other, using the postMessage API.
@@ -730,7 +731,7 @@ window.addEventListener('message', async (event) => {
             launch_msg_id: msg_id,
         };
         // launch child app
-        window.launch_app({
+        launch_app({
             name: event.data.app_name ?? app_name,
             args: event.data.args ?? {},
             parent_instance_id: event.data.appInstanceID,

+ 5 - 3
src/UI/UIDesktop.js

@@ -38,6 +38,7 @@ import UIWindowSettings from "./Settings/UIWindowSettings.js"
 import UIWindowTaskManager from "./UIWindowTaskManager.js"
 import truncate_filename from '../helpers/truncate_filename.js';
 import UINotification from "./UINotification.js"
+import launch_app from "../helpers/launch_app.js"
 
 async function UIDesktop(options){
     let h = '';
@@ -1012,8 +1013,9 @@ async function UIDesktop(options){
     else if(window.app_launched_from_url){
         let qparams = new URLSearchParams(window.location.search);      
         if(!qparams.has('c')){
-            window.launch_app({
-                name: window.app_launched_from_url,
+            launch_app({
+                app: window.app_launched_from_url.name,
+                app_obj: window.app_launched_from_url,
                 readURL: qparams.get('readURL'),
                 maximized: qparams.get('maximized'),
                 params: window.app_query_params ?? [],
@@ -1376,7 +1378,7 @@ $(document).on('click', '.refer-btn', async function(e){
 })
 
 $(document).on('click', '.start-app', async function(e){
-    window.launch_app({
+    launch_app({
         name: $(this).attr('data-app-name')
     })
     // close popovers

+ 4 - 3
src/UI/UIItem.js

@@ -27,6 +27,7 @@ import UIContextMenu from './UIContextMenu.js'
 import UIAlert from './UIAlert.js'
 import path from "../lib/path.js"
 import truncate_filename from '../helpers/truncate_filename.js';
+import launch_app from "../helpers/launch_app.js"
 
 function UIItem(options){
     const matching_appendto_count = $(options.appendTo).length;
@@ -426,7 +427,7 @@ function UIItem(options){
                 // open each item
                 for (let i = 0; i < items_to_open.length; i++) {
                     const item = items_to_open[i];
-                    window.launch_app({
+                    launch_app({
                         name: options.associated_app_name, 
                         file_path: item.path,
                         // app_obj: open_item_meta.suggested_apps[0],
@@ -1038,7 +1039,7 @@ function UIItem(options){
                                         window.mutate_user_preferences(window.user_preferences);
                                     }
                                 }
-                                window.launch_app({
+                                launch_app({
                                     name: suggested_app.name,
                                     file_path: $(el_item).attr('data-path'),
                                     window_title: $(el_item).attr('data-name'),
@@ -1143,7 +1144,7 @@ function UIItem(options){
                     html: i18n('deploy_as_app'),
                     disabled: !options.is_dir,
                     onClick: async function () {
-                        window.launch_app({
+                        launch_app({
                             name: 'dev-center',
                             file_path: $(el_item).attr('data-path'),
                             file_uid: $(el_item).attr('data-uid'),

+ 5 - 4
src/UI/UITaskbar.js

@@ -19,6 +19,7 @@
 
 import UITaskbarItem from './UITaskbarItem.js'
 import UIPopover from './UIPopover.js'
+import launch_app from "../helpers/launch_app.js"
 
 async function UITaskbar(options){
     window.global_element_id++;
@@ -175,7 +176,7 @@ async function UITaskbar(options){
         onClick: function(){
             let open_window_count = parseInt($(`.taskbar-item[data-app="explorer"]`).attr('data-open-windows'));
             if(open_window_count === 0){
-                window.launch_app({ name: 'explorer', path: window.home_path});
+                launch_app({ name: 'explorer', path: window.home_path});
             }else{
                 return false;
             }
@@ -197,7 +198,7 @@ async function UITaskbar(options){
                 onClick: function(){
                     let open_window_count = parseInt($(`.taskbar-item[data-app="${app_info.name}"]`).attr('data-open-windows'));
                     if(open_window_count === 0){
-                        window.launch_app({
+                        launch_app({
                             name: app_info.name,
                         }) 
                     }else{
@@ -226,7 +227,7 @@ async function UITaskbar(options){
         onClick: function(){
             let open_windows = $(`.window[data-path="${html_encode(window.trash_path)}"]`);
             if(open_windows.length === 0){
-                window.launch_app({ name: 'explorer', path: window.trash_path});
+                launch_app({ name: 'explorer', path: window.trash_path});
             }else{
                 open_windows.focusWindow();
             }
@@ -279,7 +280,7 @@ window.make_taskbar_sortable = function(){
                     onClick: function(){
                         let open_window_count = parseInt($(`.taskbar-item[data-app="${$(ui.item).attr('data-app-name')}"]`).attr('data-open-windows'));
                         if(open_window_count === 0){
-                            window.launch_app({
+                            launch_app({
                                 name: $(ui.item).attr('data-app-name'),
                             }) 
                         }else{

+ 4 - 3
src/UI/UITaskbarItem.js

@@ -19,6 +19,7 @@
 
 import UIContextMenu from './UIContextMenu.js';
 import path from '../lib/path.js';
+import launch_app from "../helpers/launch_app.js"
 
 let tray_item_id = 1;
 
@@ -122,7 +123,7 @@ function UITaskbarItem(options){
                 val: $(this).attr('data-id'),
                 onClick: function(){
                     // is trash?
-                    window.launch_app({
+                    launch_app({
                         name: options.app,
                         maximized: (isMobile.phone || isMobile.tablet),
                     })
@@ -137,7 +138,7 @@ function UITaskbarItem(options){
                 html: 'Open Trash',
                 val: $(this).attr('data-id'),
                 onClick: function(){
-                    window.launch_app({
+                    launch_app({
                         name: options.app,
                         path: window.trash_path,
                         maximized: (isMobile.phone || isMobile.tablet),
@@ -310,7 +311,7 @@ function UITaskbarItem(options){
                 // open each item
                 for (let i = 0; i < items_to_sign.length; i++) {
                     const item = items_to_sign[i];
-                    window.launch_app({
+                    launch_app({
                         name: options.app, 
                         file_path: item.path,
                         // app_obj: open_item_meta.suggested_apps[0],

+ 4 - 3
src/UI/UIWindow.js

@@ -28,6 +28,7 @@ import new_context_menu_item from '../helpers/new_context_menu_item.js';
 import refresh_item_container from '../helpers/refresh_item_container.js';
 import UIWindowSaveAccount from './UIWindowSaveAccount.js';
 import UIWindowEmailConfirmationRequired from './UIWindowEmailConfirmationRequired.js';
+import launch_app from "../helpers/launch_app.js"
 
 const el_body = document.getElementsByTagName('body')[0];
 
@@ -435,7 +436,7 @@ async function UIWindow(options) {
                 onClick: function(){
                     let open_window_count = parseInt($(`.taskbar-item[data-app="${options.app}"]`).attr('data-open-windows'));
                     if(open_window_count === 0){
-                        window.launch_app({
+                        launch_app({
                             name: options.app,
                         }) 
                     }else{
@@ -2038,7 +2039,7 @@ async function UIWindow(options) {
                             html: i18n('deploy_as_app'),
                             disabled: !options.is_dir,
                             onClick: async function () {
-                                window.launch_app({
+                                launch_app({
                                     name: 'dev-center',
                                     file_path: $(el_window).attr('data-path'),
                                     file_uid: $(el_window).attr('data-uid'),
@@ -2168,7 +2169,7 @@ async function UIWindow(options) {
         setTimeout(function(){
             window.enter_fullpage_mode(el_window);
             $(el_window).show()
-        }, 5);
+        }, 50);
     }
 
     return el_window;

+ 2 - 0
src/globals.js

@@ -151,6 +151,8 @@ window.original_window_position = {};
 
 // recalculate desktop height and width on window resize
 $( window ).on( "resize", function() {
+    if(window.is_fullpage_mode) return;
+
     const new_desktop_height = window.innerHeight - window.toolbar_height - window.taskbar_height;
     const new_desktop_width = window.innerWidth;
 

+ 4 - 298
src/helpers.js

@@ -28,8 +28,8 @@ import update_username_in_gui from './helpers/update_username_in_gui.js';
 import update_title_based_on_uploads from './helpers/update_title_based_on_uploads.js';
 import content_type_to_icon from './helpers/content_type_to_icon.js';
 import truncate_filename from './helpers/truncate_filename.js';
-import { PROCESS_RUNNING, PortalProcess, PseudoProcess } from "./definitions.js";
 import UIWindowProgress from './UI/UIWindowProgress.js';
+import launch_app from "./helpers/launch_app.js";
 
 window.is_auth = ()=>{
     if(localStorage.getItem("auth_token") === null || window.auth_token === null)
@@ -1565,300 +1565,6 @@ window.trigger_download = (paths)=>{
     });
 }
 
-/**
- * 
- * @param {*} options 
- */
-window.launch_app = async (options)=>{
-    const uuid = options.uuid ?? window.uuidv4();
-    let icon, title, file_signature;
-    const window_options = options.window_options ?? {};
-
-    if (options.parent_instance_id) {
-        window_options.parent_instance_id = options.parent_instance_id;
-    }
-
-    // try to get 3rd-party app info
-    let app_info = options.app_obj ?? await window.get_apps(options.name);
-
-    //-----------------------------------
-    // icon
-    //-----------------------------------
-    if(app_info.icon)
-        icon = app_info.icon;
-    else if(options.name === 'explorer')
-        icon = window.icons['folder.svg'];
-    else
-        icon = window.icons['app-icon-'+options.name+'.svg']
-
-    //-----------------------------------
-    // title
-    //-----------------------------------
-    if(app_info.title)
-        title = app_info.title;
-    else if(options.window_title)
-        title = options.window_title;
-    else if(options.name)
-        title = options.name;
-
-    //-----------------------------------
-    // maximize on start
-    //-----------------------------------
-    if(app_info.maximize_on_start && app_info.maximize_on_start === 1)
-        options.maximized = 1;
-
-    //-----------------------------------
-    // if opened a file, sign it
-    //-----------------------------------
-    if(options.file_signature)
-        file_signature = options.file_signature;
-    else if(options.file_uid){
-        file_signature = await puter.fs.sign(app_info.uuid, {uid: options.file_uid, action: 'write'});
-        // add token to options
-        options.token = file_signature.token;
-        // add file_signature to options
-        file_signature = file_signature.items;
-    }
-
-    // -----------------------------------
-    // Create entry to track the "portal"
-    // (portals are processese in Puter's GUI)
-    // -----------------------------------
-
-    let el_win;
-    let process;
-
-    //------------------------------------
-    // Explorer
-    //------------------------------------
-    if(options.name === 'explorer' || options.name === 'trash'){
-        process = new PseudoProcess({
-            uuid,
-            name: 'explorer',
-            parent: options.parent_instance_id,
-            meta: {
-                launch_options: options,
-                app_info: app_info,
-            }
-        });
-        const svc_process = globalThis.services.get('process');
-        svc_process.register(process);
-        if(options.path === window.home_path){
-            title = 'Home';
-            icon = window.icons['folder-home.svg'];
-        }
-        else if(options.path === window.trash_path){
-            title = 'Trash';
-            icon = window.icons['trash.svg'];
-        }
-        else if(!options.path)
-            title = window.root_dirname;
-        else
-            title = path.dirname(options.path);
-
-        // open window
-        el_win = UIWindow({
-            element_uuid: uuid,
-            icon: icon,
-            path: options.path ?? window.home_path,
-            title: title,
-            uid: null,
-            is_dir: true,
-            app: 'explorer',
-            ...window_options,
-            is_maximized: options.maximized,
-        });
-    }
-    //------------------------------------
-    // All other apps
-    //------------------------------------
-    else{
-        process = new PortalProcess({
-            uuid,
-            name: app_info.name,
-            parent: options.parent_instance_id,
-            meta: {
-                launch_options: options,
-                app_info: app_info,
-            }
-        });
-        const svc_process = globalThis.services.get('process');
-        svc_process.register(process);
-
-        //-----------------------------------
-        // iframe_url
-        //-----------------------------------
-        let iframe_url;
-
-        // This can be any trusted URL that won't be used for other apps
-        const BUILTIN_PREFIX = 'https://builtins.namespaces.puter.com/';
-
-        if(!app_info.index_url){
-            iframe_url = new URL('https://'+options.name+'.' + window.app_domain + `/index.html`);
-        } else if ( app_info.index_url.startsWith(BUILTIN_PREFIX) ) {
-            const name = app_info.index_url.slice(BUILTIN_PREFIX.length);
-            iframe_url = new URL(`${window.gui_origin}/builtin/${name}`);
-        } else {
-            iframe_url = new URL(app_info.index_url);
-        }
-
-        // add app_instance_id to URL
-        iframe_url.searchParams.append('puter.app_instance_id', uuid);
-
-        // add app_id to URL
-        iframe_url.searchParams.append('puter.app.id', app_info.uuid);
-
-        // add parent_app_instance_id to URL
-        if (options.parent_instance_id) {
-            iframe_url.searchParams.append('puter.parent_instance_id', options.parent_instance_id);
-        }
-
-        if(file_signature){
-            iframe_url.searchParams.append('puter.item.uid', file_signature.uid);
-            iframe_url.searchParams.append('puter.item.path', privacy_aware_path(options.file_path) || file_signature.path);
-            iframe_url.searchParams.append('puter.item.name', file_signature.fsentry_name);
-            iframe_url.searchParams.append('puter.item.read_url', file_signature.read_url);
-            iframe_url.searchParams.append('puter.item.write_url', file_signature.write_url);
-            iframe_url.searchParams.append('puter.item.metadata_url', file_signature.metadata_url);
-            iframe_url.searchParams.append('puter.item.size', file_signature.fsentry_size);
-            iframe_url.searchParams.append('puter.item.accessed', file_signature.fsentry_accessed);
-            iframe_url.searchParams.append('puter.item.modified', file_signature.fsentry_modified);
-            iframe_url.searchParams.append('puter.item.created', file_signature.fsentry_created);
-            iframe_url.searchParams.append('puter.domain', window.app_domain);
-        }
-        else if(options.readURL){
-            iframe_url.searchParams.append('puter.item.name', options.filename);
-            iframe_url.searchParams.append('puter.item.path', privacy_aware_path(options.file_path));
-            iframe_url.searchParams.append('puter.item.read_url', options.readURL);
-            iframe_url.searchParams.append('puter.domain', window.app_domain);
-        }
-
-        if (app_info.godmode && app_info.godmode === 1){
-            // Add auth_token to GODMODE apps
-
-            iframe_url.searchParams.append('puter.auth.token', window.auth_token);
-            iframe_url.searchParams.append('puter.auth.username', window.user.username);
-            iframe_url.searchParams.append('puter.domain', window.app_domain);
-        } else if (options.token){
-            // App token. Only add token if it's not a GODMODE app since GODMODE apps already have the super token
-            // that has access to everything.
-
-            iframe_url.searchParams.append('puter.auth.token', options.token);
-        } else {
-            // Try to acquire app token from the server
-
-            let response = await fetch(window.api_origin + "/auth/get-user-app-token", {
-                "headers": {
-                    "Content-Type": "application/json",
-                    "Authorization": "Bearer "+ window.auth_token,
-                },
-                "body": JSON.stringify({app_uid: app_info.uid ?? app_info.uuid}),
-                "method": "POST",
-                });
-            let res = await response.json();
-            if(res.token){
-                iframe_url.searchParams.append('puter.auth.token', res.token);
-            }
-        }
-
-        if(window.api_origin)
-            iframe_url.searchParams.append('puter.api_origin', window.api_origin);
-
-        // Add options.params to URL
-        if(options.params){
-            iframe_url.searchParams.append('puter.domain', window.app_domain);
-            for (const property in options.params) {
-                iframe_url.searchParams.append(property, options.params[property]);
-            }
-        }
-
-        // Add locale to URL
-        iframe_url.searchParams.append('puter.locale', window.locale);
-
-        // Add options.args to URL
-        iframe_url.searchParams.append('puter.args', JSON.stringify(options.args ?? {}));
-
-        // ...and finally append utm_source=puter.com to the URL
-        iframe_url.searchParams.append('utm_source', 'puter.com');
-
-        // register app_instance_uid
-        window.app_instance_ids.add(uuid);
-
-        // open window
-        el_win = UIWindow({
-            element_uuid: uuid,
-            title: title,
-            iframe_url: iframe_url.href,
-            params: options.params ?? undefined,
-            icon: icon,
-            window_class: 'window-app',
-            update_window_url: true,
-            app_uuid: app_info.uuid ?? app_info.uid,
-            top: options.maximized ? 0 : undefined,
-            left: options.maximized ? 0 : undefined,
-            height: options.maximized ? `calc(100% - ${window.taskbar_height + window.toolbar_height + 1}px)` : undefined,
-            width: options.maximized ? `100%` : undefined,
-            app: options.name,
-            is_visible: ! app_info.background,
-            is_maximized: options.maximized,
-            is_fullpage: options.is_fullpage,
-            ...window_options,
-            show_in_taskbar: app_info.background ? false : window_options?.show_in_taskbar,
-        });
-
-        if ( ! app_info.background ) {
-            $(el_win).show();
-        }
-
-        // send post request to /rao to record app open
-        if(options.name !== 'explorer'){
-            // add the app to the beginning of the array
-            window.launch_apps.recent.unshift(app_info);
-
-            // dedupe the array by uuid, uid, and id
-            window.launch_apps.recent = _.uniqBy(window.launch_apps.recent, 'name');
-
-            // limit to window.launch_recent_apps_count
-            window.launch_apps.recent = window.launch_apps.recent.slice(0, window.launch_recent_apps_count);
-
-            // send post request to /rao to record app open
-            $.ajax({
-                url: window.api_origin + "/rao",
-                type: 'POST',
-                data: JSON.stringify({ 
-                    original_client_socket_id: window.socket?.id,
-                    app_uid: app_info.uid ?? app_info.uuid,
-                }),
-                async: true,
-                contentType: "application/json",
-                headers: {
-                    "Authorization": "Bearer "+window.auth_token
-                },
-            })
-        }
-    }
-
-    (async () => {
-        const el = await el_win;
-        $(el).on('remove', () => {
-            const svc_process = globalThis.services.get('process');
-            svc_process.unregister(process.uuid);
-
-            // If it's a non-sdk app, report that it launched and closed.
-            // FIXME: This is awkward. Really, we want some way of knowing when it's launched and reporting that immediately instead.
-            const $app_iframe = $(el).find('.window-app-iframe');
-            if ($app_iframe.attr('data-appUsesSdk') !== 'true') {
-                window.report_app_launched(process.uuid, { uses_sdk: false });
-                // We also have to report an extra close event because the real one was sent already
-                window.report_app_closed(process.uuid);
-            }
-        });
-
-        process.references.el_win = el;
-        process.chstatus(PROCESS_RUNNING);
-    })();
-}
-
 window.open_item = async function(options){
     let el_item = options.item;
     const $el_parent_window = $(el_item).closest('.window');
@@ -1968,7 +1674,7 @@ window.open_item = async function(options){
     // Does the user have a preference for this file type?
     //----------------------------------------------------------------
     else if(!associated_app_name && !is_dir && window.user_preferences[`default_apps${path.extname(item_path).toLowerCase()}`]) {
-        window.launch_app({
+        launch_app({
             name: window.user_preferences[`default_apps${path.extname(item_path).toLowerCase()}`],
             file_path: item_path,
             window_title: path.basename(item_path),
@@ -1980,7 +1686,7 @@ window.open_item = async function(options){
     // Is there an app associated with this item?
     //----------------------------------------------------------------
     else if(associated_app_name !== ''){
-        window.launch_app({
+        launch_app({
             name: associated_app_name,
         })
     }
@@ -2079,7 +1785,7 @@ window.open_item = async function(options){
         // First suggested app is default app to open this item
         //---------------------------------------------
         else{
-            window.launch_app({
+            launch_app({
                 name: suggested_apps[0].name, 
                 token: open_item_meta.token,
                 file_path: item_path,

+ 327 - 0
src/helpers/launch_app.js

@@ -0,0 +1,327 @@
+/**
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import path from "../lib/path.js"
+import { PROCESS_RUNNING, PortalProcess, PseudoProcess } from "../definitions.js";
+import UIWindow from "../UI/UIWindow.js";
+
+/**
+ * Launches an app. 
+ * 
+ * @param {*} options.name - The name of the app to launch.
+ */
+const launch_app = async (options)=>{
+    console.log('launch_app', options);
+    const uuid = options.uuid ?? window.uuidv4();
+    let icon, title, file_signature;
+    const window_options = options.window_options ?? {};
+
+    if (options.parent_instance_id) {
+        window_options.parent_instance_id = options.parent_instance_id;
+    }
+
+    // If the app object is not provided, get it from the server
+    let app_info = options.app_obj ?? await window.get_apps(options.name);
+
+    // For backward compatibility reasons we need to make sure that both `uuid` and `uid` are set
+    app_info.uuid = app_info.uuid ?? app_info.uid;
+    app_info.uid = app_info.uid ?? app_info.uuid;
+
+    // If no `options.name` is provided, use the app name from the app_info
+    options.name = options.name ?? app_info.name;
+
+    //-----------------------------------
+    // icon
+    //-----------------------------------
+    if(app_info.icon)
+        icon = app_info.icon;
+    else if(options.name === 'explorer')
+        icon = window.icons['folder.svg'];
+    else
+        icon = window.icons['app-icon-'+options.name+'.svg']
+
+    //-----------------------------------
+    // title
+    //-----------------------------------
+    if(app_info.title)
+        title = app_info.title;
+    else if(options.window_title)
+        title = options.window_title;
+    else if(options.name)
+        title = options.name;
+
+    //-----------------------------------
+    // maximize on start
+    //-----------------------------------
+    if(app_info.maximize_on_start && app_info.maximize_on_start === 1)
+        options.maximized = 1;
+
+    //-----------------------------------
+    // if opened a file, sign it
+    //-----------------------------------
+    if(options.file_signature)
+        file_signature = options.file_signature;
+    else if(options.file_uid){
+        file_signature = await puter.fs.sign(app_info.uuid, {uid: options.file_uid, action: 'write'});
+        // add token to options
+        options.token = file_signature.token;
+        // add file_signature to options
+        file_signature = file_signature.items;
+    }
+
+    // -----------------------------------
+    // Create entry to track the "portal"
+    // (portals are processese in Puter's GUI)
+    // -----------------------------------
+
+    let el_win;
+    let process;
+
+    //------------------------------------
+    // Explorer
+    //------------------------------------
+    if(options.name === 'explorer' || options.name === 'trash'){
+        process = new PseudoProcess({
+            uuid,
+            name: 'explorer',
+            parent: options.parent_instance_id,
+            meta: {
+                launch_options: options,
+                app_info: app_info,
+            }
+        });
+        const svc_process = globalThis.services.get('process');
+        svc_process.register(process);
+        if(options.path === window.home_path){
+            title = 'Home';
+            icon = window.icons['folder-home.svg'];
+        }
+        else if(options.path === window.trash_path){
+            title = 'Trash';
+            icon = window.icons['trash.svg'];
+        }
+        else if(!options.path)
+            title = window.root_dirname;
+        else
+            title = path.dirname(options.path);
+
+        // open window
+        el_win = UIWindow({
+            element_uuid: uuid,
+            icon: icon,
+            path: options.path ?? window.home_path,
+            title: title,
+            uid: null,
+            is_dir: true,
+            app: 'explorer',
+            ...window_options,
+            is_maximized: options.maximized,
+        });
+    }
+    //------------------------------------
+    // All other apps
+    //------------------------------------
+    else{
+        process = new PortalProcess({
+            uuid,
+            name: app_info.name,
+            parent: options.parent_instance_id,
+            meta: {
+                launch_options: options,
+                app_info: app_info,
+            }
+        });
+        const svc_process = globalThis.services.get('process');
+        svc_process.register(process);
+
+        //-----------------------------------
+        // iframe_url
+        //-----------------------------------
+        let iframe_url;
+
+        // This can be any trusted URL that won't be used for other apps
+        const BUILTIN_PREFIX = 'https://builtins.namespaces.puter.com/';
+
+        if(!app_info.index_url){
+            iframe_url = new URL('https://'+options.name+'.' + window.app_domain + `/index.html`);
+        } else if ( app_info.index_url.startsWith(BUILTIN_PREFIX) ) {
+            const name = app_info.index_url.slice(BUILTIN_PREFIX.length);
+            iframe_url = new URL(`${window.gui_origin}/builtin/${name}`);
+        } else {
+            iframe_url = new URL(app_info.index_url);
+        }
+
+        // add app_instance_id to URL
+        iframe_url.searchParams.append('puter.app_instance_id', uuid);
+
+        // add app_id to URL
+        iframe_url.searchParams.append('puter.app.id', app_info.uuid);
+
+        // add parent_app_instance_id to URL
+        if (options.parent_instance_id) {
+            iframe_url.searchParams.append('puter.parent_instance_id', options.parent_instance_id);
+        }
+
+        if(file_signature){
+            iframe_url.searchParams.append('puter.item.uid', file_signature.uid);
+            iframe_url.searchParams.append('puter.item.path', privacy_aware_path(options.file_path) || file_signature.path);
+            iframe_url.searchParams.append('puter.item.name', file_signature.fsentry_name);
+            iframe_url.searchParams.append('puter.item.read_url', file_signature.read_url);
+            iframe_url.searchParams.append('puter.item.write_url', file_signature.write_url);
+            iframe_url.searchParams.append('puter.item.metadata_url', file_signature.metadata_url);
+            iframe_url.searchParams.append('puter.item.size', file_signature.fsentry_size);
+            iframe_url.searchParams.append('puter.item.accessed', file_signature.fsentry_accessed);
+            iframe_url.searchParams.append('puter.item.modified', file_signature.fsentry_modified);
+            iframe_url.searchParams.append('puter.item.created', file_signature.fsentry_created);
+            iframe_url.searchParams.append('puter.domain', window.app_domain);
+        }
+        else if(options.readURL){
+            iframe_url.searchParams.append('puter.item.name', options.filename);
+            iframe_url.searchParams.append('puter.item.path', privacy_aware_path(options.file_path));
+            iframe_url.searchParams.append('puter.item.read_url', options.readURL);
+            iframe_url.searchParams.append('puter.domain', window.app_domain);
+        }
+
+        // In godmode, we add the super token to the iframe URL
+        // so that the app can access everything.
+        if (app_info.godmode && app_info.godmode === 1){
+            iframe_url.searchParams.append('puter.auth.token', window.auth_token);
+            iframe_url.searchParams.append('puter.auth.username', window.user.username);
+            iframe_url.searchParams.append('puter.domain', window.app_domain);
+        } 
+        // App token. Only add token if it's not a GODMODE app since GODMODE apps already have the super token
+        // that has access to everything.
+        else if (options.token){
+            iframe_url.searchParams.append('puter.auth.token', options.token);
+        } else {
+            // Try to acquire app token from the server
+
+            let response = await fetch(window.api_origin + "/auth/get-user-app-token", {
+                "headers": {
+                    "Content-Type": "application/json",
+                    "Authorization": "Bearer "+ window.auth_token,
+                },
+                "body": JSON.stringify({app_uid: app_info.uid ?? app_info.uuid}),
+                "method": "POST",
+                });
+            let res = await response.json();
+            if(res.token){
+                iframe_url.searchParams.append('puter.auth.token', res.token);
+            }
+        }
+
+        if(window.api_origin)
+            iframe_url.searchParams.append('puter.api_origin', window.api_origin);
+
+        // Add options.params to URL
+        if(options.params){
+            iframe_url.searchParams.append('puter.domain', window.app_domain);
+            for (const property in options.params) {
+                iframe_url.searchParams.append(property, options.params[property]);
+            }
+        }
+
+        // Add locale to URL
+        iframe_url.searchParams.append('puter.locale', window.locale);
+
+        // Add options.args to URL
+        iframe_url.searchParams.append('puter.args', JSON.stringify(options.args ?? {}));
+
+        // ...and finally append utm_source=puter.com to the URL
+        iframe_url.searchParams.append('utm_source', 'puter.com');
+
+        // register app_instance_uid
+        window.app_instance_ids.add(uuid);
+
+        // open window
+        el_win = UIWindow({
+            element_uuid: uuid,
+            title: title,
+            iframe_url: iframe_url.href,
+            params: options.params ?? undefined,
+            icon: icon,
+            window_class: 'window-app',
+            update_window_url: true,
+            app_uuid: app_info.uuid ?? app_info.uid,
+            top: options.maximized ? 0 : undefined,
+            left: options.maximized ? 0 : undefined,
+            height: options.maximized ? `calc(100% - ${window.taskbar_height + window.toolbar_height + 1}px)` : undefined,
+            width: options.maximized ? `100%` : undefined,
+            app: options.name,
+            is_visible: ! app_info.background,
+            is_maximized: options.maximized,
+            is_fullpage: options.is_fullpage,
+            ...window_options,
+            show_in_taskbar: app_info.background ? false : window_options?.show_in_taskbar,
+        });
+
+        if ( ! app_info.background ) {
+            $(el_win).show();
+        }
+
+        // send post request to /rao to record app open
+        if(options.name !== 'explorer'){
+            // add the app to the beginning of the array
+            window.launch_apps.recent.unshift(app_info);
+
+            // dedupe the array by uuid, uid, and id
+            window.launch_apps.recent = _.uniqBy(window.launch_apps.recent, 'name');
+
+            // limit to window.launch_recent_apps_count
+            window.launch_apps.recent = window.launch_apps.recent.slice(0, window.launch_recent_apps_count);
+
+            // send post request to /rao to record app open
+            $.ajax({
+                url: window.api_origin + "/rao",
+                type: 'POST',
+                data: JSON.stringify({ 
+                    original_client_socket_id: window.socket?.id,
+                    app_uid: app_info.uid ?? app_info.uuid,
+                }),
+                async: true,
+                contentType: "application/json",
+                headers: {
+                    "Authorization": "Bearer "+window.auth_token
+                },
+            })
+        }
+    }
+
+    (async () => {
+        const el = await el_win;
+        $(el).on('remove', () => {
+            const svc_process = globalThis.services.get('process');
+            svc_process.unregister(process.uuid);
+
+            // If it's a non-sdk app, report that it launched and closed.
+            // FIXME: This is awkward. Really, we want some way of knowing when it's launched and reporting that immediately instead.
+            const $app_iframe = $(el).find('.window-app-iframe');
+            if ($app_iframe.attr('data-appUsesSdk') !== 'true') {
+                window.report_app_launched(process.uuid, { uses_sdk: false });
+                // We also have to report an extra close event because the real one was sent already
+                window.report_app_closed(process.uuid);
+            }
+        });
+
+        process.references.el_win = el;
+        process.chstatus(PROCESS_RUNNING);
+    })();
+}
+
+export default launch_app;

+ 8 - 0
src/initgui.js

@@ -194,6 +194,14 @@ window.initgui = async function(options){
     if(url_paths[0]?.toLocaleLowerCase() === 'app' && url_paths[1]){
         window.app_launched_from_url = url_paths[1];
 
+        // get app metadata
+        try{
+            window.app_launched_from_url = await puter.apps.get(window.app_launched_from_url)
+            window.is_fullpage_mode = window.app_launched_from_url.metadata?.fullpage_on_landing ?? false;
+        }catch(e){
+            console.error(e);
+        }
+
         // get query params, any param that doesn't start with 'puter.' will be passed to the app
         window.app_query_params = {};
         for (let [key, value] of window.url_query_params) {

+ 2 - 1
src/keyboard.js

@@ -18,6 +18,7 @@
  */
 
 import UIAlert from './UI/UIAlert.js';
+import launch_app from './helpers/launch_app.js';
 
 $(document).bind('keydown', async function(e){
     const focused_el = document.activeElement;
@@ -626,7 +627,7 @@ $(document).bind("keyup keydown", async function(e){
         if($('.launch-app-selected').length > 0){
             // close launch menu
             $(".launch-popover").fadeOut(200, function(){
-                window.launch_app({
+                launch_app({
                     name: $('.launch-app-selected').attr('data-name'),
                 })
                 $(".launch-popover").remove();