/** * 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 . */ import path from "./lib/path.js" import mime from "./lib/mime.js"; import UIAlert from './UI/UIAlert.js' import UIItem from './UI/UIItem.js' import UIWindow from './UI/UIWindow.js' import UIWindowLogin from './UI/UIWindowLogin.js'; import UIWindowSaveAccount from './UI/UIWindowSaveAccount.js'; 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'; window.is_auth = ()=>{ if(localStorage.getItem("auth_token") === null || window.auth_token === null) return false; else return true; } window.suggest_apps_for_fsentry = async (options)=>{ let res = await $.ajax({ url: window.api_origin + "/suggest_apps", type: 'POST', contentType: "application/json", data: JSON.stringify({ uid: options.uid ?? undefined, path: options.path ?? undefined, }), headers: { "Authorization": "Bearer "+window.auth_token }, statusCode: { 401: function () { window.logout(); }, }, success: function (res){ if(options.onSuccess && typeof options.onSuccess == "function") options.onSuccess(res); } }); return res; } /** * Formats a binary-byte integer into the human-readable form with units. * * @param {integer} bytes * @returns */ window.byte_format = (bytes)=>{ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) return '0 Byte'; const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; }; /** * A function that generates a UUID (Universally Unique Identifier) using the version 4 format, * which are random UUIDs. It uses the cryptographic number generator available in modern browsers. * * The generated UUID is a 36 character string (32 alphanumeric characters separated by 4 hyphens). * It follows the pattern: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, where x is any hexadecimal digit * and y is one of 8, 9, A, or B. * * @returns {string} Returns a new UUID v4 string. * * @example * * let id = window.uuidv4(); // Generate a new UUID * */ window.uuidv4 = ()=>{ return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ); } /** * Checks if the provided string is a valid email format. * * @function * @global * @param {string} email - The email string to be validated. * @returns {boolean} `true` if the email is valid, otherwise `false`. * @example * window.is_email("test@example.com"); // true * window.is_email("invalid-email"); // false */ window.is_email = (email) => { const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return re.test(String(email).toLowerCase()); } /** * A function that scrolls the parent element so that the child element is in view. * If the child element is already in view, no scrolling occurs. * The function decides the best scroll direction based on which requires the smaller adjustment. * * @param {HTMLElement} parent - The parent HTML element that might be scrolled. * @param {HTMLElement} child - The child HTML element that should be made viewable. * * @returns {void} * * @example * * let parentElem = document.querySelector('#parent'); * let childElem = document.querySelector('#child'); * window.scrollParentToChild(parentElem, childElem); * // Scrolls parentElem so that childElem is in view * */ window.scrollParentToChild = (parent, child)=>{ // Where is the parent on page var parentRect = parent.getBoundingClientRect(); // What can you see? var parentViewableArea = { height: parent.clientHeight, width: parent.clientWidth }; // Where is the child var childRect = child.getBoundingClientRect(); // Is the child viewable? var isViewable = (childRect.top >= parentRect.top) && (childRect.bottom <= parentRect.top + parentViewableArea.height); // if you can't see the child try to scroll parent if (!isViewable) { // Should we scroll using top or bottom? Find the smaller ABS adjustment const scrollTop = childRect.top - parentRect.top; const scrollBot = childRect.bottom - parentRect.bottom; if (Math.abs(scrollTop) < Math.abs(scrollBot)) { // we're near the top of the list parent.scrollTop += (scrollTop + 80); } else { // we're near the bottom of the list parent.scrollTop += (scrollBot + 80); } } } window.getItem = async function(options){ return $.ajax({ url: window.api_origin + "/getItem", type: 'POST', data: JSON.stringify({ key: options.key, app: options.app_uid, }), async: true, contentType: "application/json", headers: { "Authorization": "Bearer "+window.auth_token }, statusCode: { 401: function () { window.logout(); }, }, success: function (result){ if(options.success && typeof(options.success) === "function") options.success(result); } }) } window.setItem = async function(options){ return $.ajax({ url: window.api_origin + "/setItem", type: 'POST', data: JSON.stringify({ app: options.app_uid, key: options.key, value: options.value, }), async: true, contentType: "application/json", headers: { "Authorization": "Bearer "+window.auth_token }, statusCode: { 401: function () { window.logout(); }, }, success: function (fsentry){ if(options.success && typeof(options.success) === "function") options.success(fsentry) } }) } /** * Converts a glob pattern to a regular expression, with optional extended or globstar matching. * * @param {string} glob - The glob pattern to convert. * @param {Object} [opts] - Optional options for the conversion. * @param {boolean} [opts.extended=false] - If true, enables extended matching with single character matching, character ranges, group matching, etc. * @param {boolean} [opts.globstar=false] - If true, uses globstar matching, where '*' matches zero or more path segments. * @param {string} [opts.flags] - Regular expression flags to include (e.g., 'i' for case-insensitive). * @returns {RegExp} The generated regular expression. * @throws {TypeError} If the provided glob pattern is not a string. */ window.globToRegExp = function (glob, opts) { if (typeof glob !== 'string') { throw new TypeError('Expected a string'); } var str = String(glob); // The regexp we are building, as a string. var reStr = ""; // Whether we are matching so called "extended" globs (like bash) and should // support single character matching, matching ranges of characters, group // matching, etc. var extended = opts ? !!opts.extended : false; // When globstar is _false_ (default), '/foo/*' is translated a regexp like // '^\/foo\/.*$' which will match any string beginning with '/foo/' // When globstar is _true_, '/foo/*' is translated to regexp like // '^\/foo\/[^/]*$' which will match any string beginning with '/foo/' BUT // which does not have a '/' to the right of it. // E.g. with '/foo/*' these will match: '/foo/bar', '/foo/bar.txt' but // these will not '/foo/bar/baz', '/foo/bar/baz.txt' // Lastely, when globstar is _true_, '/foo/**' is equivelant to '/foo/*' when // globstar is _false_ var globstar = opts ? !!opts.globstar : false; // If we are doing extended matching, this boolean is true when we are inside // a group (eg {*.html,*.js}), and false otherwise. var inGroup = false; // RegExp flags (eg "i" ) to pass in to RegExp constructor. var flags = opts && typeof (opts.flags) === "string" ? opts.flags : ""; var c; for (var i = 0, len = str.length; i < len; i++) { c = str[i]; switch (c) { case "/": case "$": case "^": case "+": case ".": case "(": case ")": case "=": case "!": case "|": reStr += "\\" + c; break; case "?": if (extended) { reStr += "."; break; } // fallthrough case "[": case "]": if (extended) { reStr += c; break; } // fallthrough case "{": if (extended) { inGroup = true; reStr += "("; break; } // fallthrough case "}": if (extended) { inGroup = false; reStr += ")"; break; } // fallthrough case ",": if (inGroup) { reStr += "|"; break; } reStr += "\\" + c; break; case "*": // Move over all consecutive "*"'s. // Also store the previous and next characters var prevChar = str[i - 1]; var starCount = 1; while (str[i + 1] === "*") { starCount++; i++; } var nextChar = str[i + 1]; if (!globstar) { // globstar is disabled, so treat any number of "*" as one reStr += ".*"; } else { // globstar is enabled, so determine if this is a globstar segment var isGlobstar = starCount > 1 // multiple "*"'s && (prevChar === "/" || prevChar === undefined) // from the start of the segment && (nextChar === "/" || nextChar === undefined) // to the end of the segment if (isGlobstar) { // it's a globstar, so match zero or more path segments reStr += "((?:[^/]*(?:/|$))*)"; i++; // move over the "/" } else { // it's not a globstar, so only match one path segment reStr += "([^/]*)"; } } break; default: reStr += c; } } // When regexp 'g' flag is specified don't // constrain the regular expression with ^ & $ if (!flags || !~flags.indexOf('g')) { reStr = "^" + reStr + "$"; } return new RegExp(reStr, flags); }; /** * Validates the provided file system entry name. * * @function validate_fsentry_name * @memberof window * @param {string} name - The name of the file system entry to validate. * @returns {boolean} Returns true if the name is valid. * @throws {Object} Throws an object with a `message` property indicating the specific validation error. * * @description * This function checks the provided name against a set of rules to determine its validity as a file system entry name: * 1. Name cannot be empty. * 2. Name must be a string. * 3. Name cannot contain the '/' character. * 4. Name cannot be the '.' character. * 5. Name cannot be the '..' character. * 6. Name cannot exceed the maximum allowed length (as defined in window.max_item_name_length). */ window.validate_fsentry_name = function(name){ if(!name) throw {message: i18n('name_cannot_be_empty')} else if(!window.isString(name)) throw {message: i18n('name_must_be_string')} else if(name.includes('/')) throw {message: i18n('name_cannot_contain_slash')} else if(name === '.') throw {message: i18n('name_cannot_contain_period')}; else if(name === '..') throw {message: i18n('name_cannot_contain_double_period')}; else if(name.length > window.max_item_name_length) throw {message: i18n('name_too_long', window.max_item_name_length)} else return true } /** * A function that generates a unique identifier by combining a random adjective, a random noun, and a random number (between 0 and 9999). * The result is returned as a string with components separated by hyphens. * It is useful when you need to create unique identifiers that are also human-friendly. * * @returns {string} A unique, hyphen-separated string comprising of an adjective, a noun, and a number. * * @example * * let identifier = window.generate_identifier(); * // identifier would be something like 'clever-idea-123' * */ window.generate_identifier = function(){ const first_adj = ['helpful','sensible', 'loyal', 'honest', 'clever', 'capable','calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy', 'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite', 'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold']; const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen', 'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree', 'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain', 'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle', 'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck', 'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly', 'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal', 'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp'] // return a random combination of first_adj + noun + number (between 0 and 9999) // e.g. clever-idea-123 return first_adj[Math.floor(Math.random() * first_adj.length)] + '-' + nouns[Math.floor(Math.random() * nouns.length)] + '-' + Math.floor(Math.random() * 10000); } /** * Checks if the provided variable is a string or an instance of the String object. * * @param {*} variable - The variable to check. * @returns {boolean} True if the variable is a string or an instance of the String object, false otherwise. */ window.isString = function (variable) { return typeof variable === 'string' || variable instanceof String; } /** * A function that checks whether a file system entry (fsentry) matches a list of allowed file types. * It handles both file extensions (like '.jpg') and MIME types (like 'text/plain'). * If the allowed file types string is empty or not provided, the function always returns true. * It checks the file types only if the fsentry is a file, not a directory. * * @param {Object} fsentry - The file system entry to check. It must be an object with properties: 'is_dir', 'name', 'type'. * @param {string} allowed_file_types_string - The list of allowed file types, separated by commas. Can include extensions and MIME types. * * @returns {boolean} True if the fsentry matches one of the allowed file types, or if the allowed_file_types_string is empty or not provided. False otherwise. * * @example * * let fsentry = {is_dir: false, name: 'example.jpg', type: 'image/jpeg'}; * let allowedTypes = '.jpg, text/plain, image/*'; * let result = window.check_fsentry_against_allowed_file_types_string(fsentry, allowedTypes); * // result would be true, as 'example.jpg' matches the '.jpg' in allowedTypes * */ window.check_fsentry_against_allowed_file_types_string =function (fsentry, allowed_file_types_string) { // simple cases that are always a pass if(!allowed_file_types_string || allowed_file_types_string.trim() === '') return true; // parse allowed_file_types into an array of extensions and types let allowed_file_types = allowed_file_types_string.split(','); if(allowed_file_types.length > 0){ // trim every entry for (let index = 0; index < allowed_file_types.length; index++) { allowed_file_types[index] = allowed_file_types[index].trim(); } } let passes_allowed_file_type_filter = true; // check types, only if this fsentry is a file and not a directory if(!fsentry.is_dir && allowed_file_types.length > 0){ passes_allowed_file_type_filter = false; for (let index = 0; index < allowed_file_types.length; index++) { const allowed_file_type = allowed_file_types[index].toLowerCase(); // if type is not already set, try to set it based on the file name if(!fsentry.type) fsentry.type = mime.getType(fsentry.name); // extensions (e.g. .jpg) if(allowed_file_type.startsWith('.') && fsentry.name.toLowerCase().endsWith(allowed_file_type)){ passes_allowed_file_type_filter = true; break; } // MIME types (e.g. text/plain) else if(window.globToRegExp(allowed_file_type).test(fsentry.type?.toLowerCase())){ passes_allowed_file_type_filter = true; break; } } } return passes_allowed_file_type_filter; } // @author Rich Adams // Implements a tap and hold functionality. If you click/tap and release, it will trigger a normal // click event. But if you click/tap and hold for 1s (default), it will trigger a taphold event instead. ;(function($) { // Default options var defaults = { duration: 500, // ms clickHandler: null } // When start of a taphold event is triggered. function startHandler(event) { var $elem = jQuery(this); // Merge the defaults and any user defined settings. let settings = jQuery.extend({}, defaults, event.data); // If object also has click handler, store it and unbind. Taphold will trigger the // click itself, rather than normal propagation. if (typeof $elem.data("events") != "undefined" && typeof $elem.data("events").click != "undefined") { // Find the one without a namespace defined. for (var c in $elem.data("events").click) { if ($elem.data("events").click[c].namespace == "") { var handler = $elem.data("events").click[c].handler $elem.data("taphold_click_handler", handler); $elem.unbind("click", handler); break; } } } // Otherwise, if a custom click handler was explicitly defined, then store it instead. else if (typeof settings.clickHandler == "function") { $elem.data("taphold_click_handler", settings.clickHandler); } // Reset the flags $elem.data("taphold_triggered", false); // If a hold was triggered $elem.data("taphold_clicked", false); // If a click was triggered $elem.data("taphold_cancelled", false); // If event has been cancelled. // Set the timer for the hold event. $elem.data("taphold_timer", setTimeout(function() { // If event hasn't been cancelled/clicked already, then go ahead and trigger the hold. if (!$elem.data("taphold_cancelled") && !$elem.data("taphold_clicked")) { // Trigger the hold event, and set the flag to say it's been triggered. $elem.trigger(jQuery.extend(event, jQuery.Event("taphold"))); $elem.data("taphold_triggered", true); } }, settings.duration)); } // When user ends a tap or click, decide what we should do. function stopHandler(event) { var $elem = jQuery(this); // If taphold has been cancelled, then we're done. if ($elem.data("taphold_cancelled")) { return; } // Clear the hold timer. If it hasn't already triggered, then it's too late anyway. clearTimeout($elem.data("taphold_timer")); // If hold wasn't triggered and not already clicked, then was a click event. if (!$elem.data("taphold_triggered") && !$elem.data("taphold_clicked")) { // If click handler, trigger it. if (typeof $elem.data("taphold_click_handler") == "function") { $elem.data("taphold_click_handler")(jQuery.extend(event, jQuery.Event("click"))); } // Set flag to say we've triggered the click event. $elem.data("taphold_clicked", true); } } // If a user prematurely leaves the boundary of the object we're working on. function leaveHandler(event) { // Cancel the event. $(this).data("taphold_cancelled", true); } // Determine if touch events are supported. var touchSupported = ("ontouchstart" in window) // Most browsers || ("onmsgesturechange" in window); // Microsoft var taphold = $.event.special.taphold = { setup: function(data) { $(this).bind((touchSupported ? "touchstart" : "mousedown"), data, startHandler) .bind((touchSupported ? "touchend" : "mouseup"), stopHandler) .bind((touchSupported ? "touchmove touchcancel" : "mouseleave"), leaveHandler); }, teardown: function(namespaces) { $(this).unbind((touchSupported ? "touchstart" : "mousedown"), startHandler) .unbind((touchSupported ? "touchend" : "mouseup"), stopHandler) .unbind((touchSupported ? "touchmove touchcancel" : "mouseleave"), leaveHandler); } }; })(jQuery); window.refresh_user_data = async (auth_token)=>{ let whoami try{ whoami = await puter.os.user(); }catch(e){ // Ignored } // update local user data if(whoami){ window.update_auth_data(auth_token, whoami) } } window.update_auth_data = (auth_token, user)=>{ window.auth_token = auth_token; localStorage.setItem('auth_token', auth_token); // Has username changed? if(window.user?.username !== user.username) update_username_in_gui(user.username); // Has email changed? if(window.user?.email !== user.email && user.email){ $('.user-email').html(html_encode(user.email)); } const to_storable_user = user => { const storable_user = {...user}; delete storable_user.taskbar_items; return storable_user; }; // update this session's user data window.user = user; localStorage.setItem('user', JSON.stringify(to_storable_user(user))); // re-initialize the Puter.js objects with the new auth token puter.setAuthToken(auth_token, window.api_origin) //update the logged_in_users array entry for this user if(window.user){ let logged_in_users_updated = false; for (let i = 0; i < window.logged_in_users.length && !logged_in_users_updated; i++) { if(window.logged_in_users[i].uuid === window.user.uuid){ window.logged_in_users[i] = window.user; window.logged_in_users[i].auth_token = window.auth_token; logged_in_users_updated = true; } } // no matching array elements, add one if(!logged_in_users_updated){ let userobj = window.user; userobj.auth_token = window.auth_token; window.logged_in_users.push(userobj); } // update local storage localStorage.setItem('logged_in_users', JSON.stringify( window.logged_in_users.map(to_storable_user))); } window.desktop_path = '/' + window.user.username + '/Desktop'; window.trash_path = '/' + window.user.username + '/Trash'; window.appdata_path = '/' + window.user.username + '/AppData'; window.docs_path = '/' + window.user.username + '/Documents'; window.pictures_path = '/' + window.user.username + '/Pictures'; window.videos_path = '/' + window.user.username + '/Videos'; window.desktop_path = '/' + window.user.username + '/Desktop'; window.home_path = '/' + window.user.username; if(window.user !== null && !window.user.is_temp){ $('.user-options-login-btn, .user-options-create-account-btn').hide(); $('.user-options-menu-btn').show(); } } window.mutate_user_preferences = function(user_preferences_delta) { for (const [key, value] of Object.entries(user_preferences_delta)) { // Don't wait for set to be done for better efficiency puter.kv.set(`user_preferences.${key}`, value); } // There may be syncing issues across multiple devices window.update_user_preferences({ ...window.user_preferences, ...user_preferences_delta }); } window.update_user_preferences = function(user_preferences) { window.user_preferences = user_preferences; localStorage.setItem('user_preferences', JSON.stringify(user_preferences)); const language = user_preferences.language ?? 'en'; window.locale = language; // Broadcast locale change to apps const broadcastService = globalThis.services.get('broadcast'); broadcastService.sendBroadcast('localeChanged', { language: language, }, { sendToNewAppInstances: true }); } window.sendWindowWillCloseMsg = function(iframe_element) { return new Promise(function(resolve){ const msg_id = window.uuidv4(); iframe_element.contentWindow.postMessage({ msg: "windowWillClose", msg_id: msg_id }, '*'); //register callback window.appCallbackFunctions[msg_id] = resolve; }) } window.logout = ()=>{ console.log('DISP LOGOUT EVENT'); $(document).trigger('logout'); // document.dispatchEvent(new Event("logout", { bubbles: true})); } /** * Checks if the current document is in fullscreen mode. * * @function is_fullscreen * @memberof window * @returns {boolean} Returns true if the document is in fullscreen mode, otherwise false. * * @example * // Checks if the document is currently in fullscreen mode * const inFullscreen = window.is_fullscreen(); * * @description * This function checks various browser-specific properties to determine if the document * is currently being displayed in fullscreen mode. It covers standard as well as * some vendor-prefixed properties to ensure compatibility across different browsers. */ window.is_fullscreen = ()=>{ return (document.fullscreenElement && document.fullscreenElement !== null) || (document.webkitIsFullScreen && document.webkitIsFullScreen !== null) || (document.webkitFullscreenElement && document.webkitFullscreenElement !== null) || (document.mozFullScreenElement && document.mozFullScreenElement !== null) || (document.msFullscreenElement && document.msFullscreenElement !== null); } window.get_apps = async (app_names, callback)=>{ if(Array.isArray(app_names)) app_names = app_names.join('|'); // 'explorer' is a special app, no metadata should be returned if(app_names === 'explorer') return []; let res = await $.ajax({ url: window.api_origin + "/apps/"+app_names, type: 'GET', async: true, contentType: "application/json", headers: { "Authorization": "Bearer "+window.auth_token }, success: function (res){ } }); if(res.length === 1) res = res[0]; if(callback && typeof callback === 'function') callback(res); else return res; } /** * Sends an "itemChanged" event to all watching applications associated with a specific item. * * @function sendItemChangeEventToWatchingApps * @memberof window * @param {string} item_uid - Unique identifier of the item that experienced the change. * @param {Object} event_data - Additional data about the event to be passed to the watching applications. * * @description * This function sends an "itemChanged" message to all applications that are currently watching * the specified item. If an application's iframe is not found or no longer valid, * it is removed from the list of watchers. * * The function expects that `window.watchItems` contains a mapping of item UIDs to arrays of app instance IDs. * * @example * // Example usage to send a change event to watching applications of an item with UID "item123". * window.sendItemChangeEventToWatchingApps('item123', { property: 'value' }); */ window.sendItemChangeEventToWatchingApps = function(item_uid, event_data){ if(window.watchItems[item_uid]){ window.watchItems[item_uid].forEach(app_instance_id => { const iframe = $(`.window[data-element_uuid="${app_instance_id}"]`).find('.window-app-iframe') if(iframe && iframe.length > 0){ iframe.get(0)?.contentWindow .postMessage({ msg: 'itemChanged', data: event_data, }, '*'); }else{ window.watchItems[item_uid].splice(window.watchItems[item_uid].indexOf(app_instance_id), 1); } }); } } /** * Assigns an icon to a filesystem entry based on its properties such as name, type, * and whether it's a directory, app, trashed, or specific file type. * * @function item_icon * @global * @async * @param {Object} fsentry - A filesystem entry object. It may contain various properties * like name, type, path, associated_app, thumbnail, is_dir, and metadata, depending on * the type of filesystem entry. */ window.item_icon = async (fsentry)=>{ // -------------------------------------------------- // If this file is Trashed then set the name to the original name of the file before it was trashed // -------------------------------------------------- if(fsentry.path?.startsWith(window.trash_path + '/')){ if(fsentry.metadata){ try{ let metadata = JSON.parse(fsentry.metadata); fsentry.name = (metadata && metadata.original_name) ? metadata.original_name : fsentry.name } catch(e){ // Ignored } } } // -------------------------------------------------- // thumbnail // -------------------------------------------------- if(fsentry.thumbnail){ return {image: fsentry.thumbnail, type: 'thumb'}; } // -------------------------------------------------- // app icon // -------------------------------------------------- else if(fsentry.associated_app && fsentry.associated_app?.name){ if(fsentry.associated_app.icon) return {image: fsentry.associated_app.icon, type: 'icon'}; else return {image: window.icons['app.svg'], type: 'icon'}; } // -------------------------------------------------- // Trash // -------------------------------------------------- else if(fsentry.shortcut_to_path && fsentry.shortcut_to_path === window.trash_path){ // get trash image, this is needed to get the correct empty vs full trash icon let trash_img = $(`.item[data-path="${html_encode(window.trash_path)}" i] .item-icon-icon`).attr('src') // if trash_img is undefined that's probably because trash wasn't added anywhere, do a direct lookup to see if trash is empty or no if(!trash_img){ let trashstat = await puter.fs.stat(window.trash_path); if(trashstat.is_empty !== undefined && trashstat.is_empty === true) trash_img = window.icons['trash.svg']; else trash_img = window.icons['trash-full.svg']; } return {image: trash_img, type: 'icon'}; } // -------------------------------------------------- // Directories // -------------------------------------------------- else if(fsentry.is_dir){ // System Directories if(fsentry.path === window.docs_path) return {image: window.icons['folder-documents.svg'], type: 'icon'}; else if (fsentry.path === window.pictures_path) return { image: window.icons['folder-pictures.svg'], type: 'icon' }; else if (fsentry.path === window.home_path) return { image: window.icons['folder-home.svg'], type: 'icon' }; else if (fsentry.path === window.videos_path) return { image: window.icons['folder-videos.svg'], type: 'icon' }; else if (fsentry.path === window.desktop_path) return { image: window.icons['folder-desktop.svg'], type: 'icon' }; // regular directories else return {image: window.icons['folder.svg'], type: 'icon'}; } // -------------------------------------------------- // Match icon by file extension // -------------------------------------------------- // *.doc else if(fsentry.name.toLowerCase().endsWith('.doc')){ return {image: window.icons['file-doc.svg'], type: 'icon'}; } // *.docx else if(fsentry.name.toLowerCase().endsWith('.docx')){ return {image: window.icons['file-docx.svg'], type: 'icon'}; } // *.exe else if(fsentry.name.toLowerCase().endsWith('.exe')){ return {image: window.icons['file-exe.svg'], type: 'icon'}; } // *.gz else if(fsentry.name.toLowerCase().endsWith('.gz')){ return {image: window.icons['file-gzip.svg'], type: 'icon'}; } // *.jar else if(fsentry.name.toLowerCase().endsWith('.jar')){ return {image: window.icons['file-jar.svg'], type: 'icon'}; } // *.java else if(fsentry.name.toLowerCase().endsWith('.java')){ return {image: window.icons['file-java.svg'], type: 'icon'}; } // *.jsp else if(fsentry.name.toLowerCase().endsWith('.jsp')){ return {image: window.icons['file-jsp.svg'], type: 'icon'}; } // *.log else if(fsentry.name.toLowerCase().endsWith('.log')){ return {image: window.icons['file-log.svg'], type: 'icon'}; } // *.mp3 else if(fsentry.name.toLowerCase().endsWith('.mp3')){ return {image: window.icons['file-mp3.svg'], type: 'icon'}; } // *.rb else if(fsentry.name.toLowerCase().endsWith('.rb')){ return {image: window.icons['file-ruby.svg'], type: 'icon'}; } // *.rss else if(fsentry.name.toLowerCase().endsWith('.rss')){ return {image: window.icons['file-rss.svg'], type: 'icon'}; } // *.rtf else if(fsentry.name.toLowerCase().endsWith('.rtf')){ return {image: window.icons['file-rtf.svg'], type: 'icon'}; } // *.sketch else if(fsentry.name.toLowerCase().endsWith('.sketch')){ return {image: window.icons['file-sketch.svg'], type: 'icon'}; } // *.sql else if(fsentry.name.toLowerCase().endsWith('.sql')){ return {image: window.icons['file-sql.svg'], type: 'icon'}; } // *.tif else if(fsentry.name.toLowerCase().endsWith('.tif')){ return {image: window.icons['file-tif.svg'], type: 'icon'}; } // *.tiff else if(fsentry.name.toLowerCase().endsWith('.tiff')){ return {image: window.icons['file-tiff.svg'], type: 'icon'}; } // *.wav else if(fsentry.name.toLowerCase().endsWith('.wav')){ return {image: window.icons['file-wav.svg'], type: 'icon'}; } // *.cpp else if(fsentry.name.toLowerCase().endsWith('.cpp')){ return {image: window.icons['file-cpp.svg'], type: 'icon'}; } // *.pptx else if(fsentry.name.toLowerCase().endsWith('.pptx')){ return {image: window.icons['file-pptx.svg'], type: 'icon'}; } // *.psd else if(fsentry.name.toLowerCase().endsWith('.psd')){ return {image: window.icons['file-psd.svg'], type: 'icon'}; } // *.py else if(fsentry.name.toLowerCase().endsWith('.py')){ return {image: window.icons['file-py.svg'], type: 'icon'}; } // *.xlsx else if(fsentry.name.toLowerCase().endsWith('.xlsx')){ return {image: window.icons['file-xlsx.svg'], type: 'icon'}; } // -------------------------------------------------- // Determine icon by set or derived mime type // -------------------------------------------------- else if(fsentry.type){ return {image: content_type_to_icon(fsentry.type), type: 'icon'}; } else{ return {image: content_type_to_icon(mime.getType(fsentry.name)), type: 'icon'}; } } /** * Asynchronously checks if a save account notice should be shown to the user, and if needed, displays the notice. * * This function first retrieves a key value pair from the cloud key-value storage to determine if the notice has been shown before. * If the notice hasn't been shown and the user is using a temporary session, the notice is then displayed. After the notice is shown, * the function updates the key-value storage indicating that the notice has been shown. The user can choose to save the session, * remind later or log in to an existing account. * * @param {string} [message] - The custom message to be displayed in the notice. If not provided, a default message will be used. * @global * @function window.show_save_account_notice_if_needed */ window.show_save_account_notice_if_needed = function(message){ window.getItem({ key: "save_account_notice_shown", success: async function(value){ if(!value && window.user?.is_temp){ window.setItem({key: "save_account_notice_shown", value: true}); // Show the notice setTimeout(async () => { const alert_resp = await UIAlert({ message: message ?? `Congrats on storing data!

Don't forget to save your session! You are in a temporary session. Save session to avoid accidentally losing your work.

`, body_icon: window.icons['reminder.svg'], buttons:[ { label: i18n('save_session'), value: 'save-session', type: 'primary', }, // { // label: 'Log into an existing account', // value: 'login', // }, { label: `I'll do it later`, value: 'remind-later', }, ], window_options: { backdrop: true, close_on_backdrop_click: false, } }) if(alert_resp === 'remind-later'){ // TODO } if(alert_resp === 'save-session'){ let saved = await UIWindowSaveAccount({ send_confirmation_code: false, }); }else if (alert_resp === 'login'){ let login_result = await UIWindowLogin({ show_signup_button: false, reload_on_success: true, send_confirmation_code: false, window_options: { show_in_taskbar: false, backdrop: true, close_on_backdrop_click: false, } }); // FIXME: Report login error. } }, window.desktop_loading_fade_delay + 1000); } } }) } window.onpopstate = (event) => { if(event.state !== null && event.state.window_id !== null){ $(`.window[data-id="${event.state.window_id}"]`).focusWindow(); } } window.sort_items = (item_container, sort_by, sort_order)=>{ if(sort_order !== 'asc' && sort_order !== 'desc') sort_order = 'asc'; $(item_container).find(`.item[data-sortable="true"]`).detach().sort(function(a,b) { // Name if(!sort_by || sort_by === 'name'){ if(a.dataset.name.toLowerCase() < b.dataset.name.toLowerCase()) { return (sort_order === 'asc' ? -1 : 1); } if(a.dataset.name.toLowerCase() > b.dataset.name.toLowerCase()) { return (sort_order === 'asc' ? 1 : -1); } return 0; } // Size else if(sort_by === 'size'){ if( parseInt(a.dataset.size) < parseInt(b.dataset.size)) { return (sort_order === 'asc' ? -1 : 1); } if( parseInt(a.dataset.size) > parseInt(b.dataset.size)) { return (sort_order === 'asc' ? 1 : -1); } return 0; } // Modified else if(sort_by === 'modified'){ if( parseInt(a.dataset.modified) < parseInt(b.dataset.modified)) { return (sort_order === 'asc' ? -1 : 1); } if( parseInt(a.dataset.modified) > parseInt(b.dataset.modified)) { return (sort_order === 'asc' ? 1 : -1); } return 0; } // Type else if(sort_by === 'type'){ if(path.extname(a.dataset.name.toLowerCase()) < path.extname(b.dataset.name.toLowerCase())) { return (sort_order === 'asc' ? -1 : 1); } if(path.extname(a.dataset.name.toLowerCase()) > path.extname(b.dataset.name.toLowerCase())) { return (sort_order === 'asc' ? 1 : -1); } return 0; } }).appendTo(item_container); } window.show_or_hide_files = (item_containers) => { const show_hidden_files = window.user_preferences.show_hidden_files; const class_to_add = show_hidden_files ? 'item-revealed' : 'item-hidden'; const class_to_remove = show_hidden_files ? 'item-hidden' : 'item-revealed'; $(item_containers) .find('.item') .filter((_, item) => item.dataset.name.startsWith('.')) .removeClass(class_to_remove).addClass(class_to_add); } window.create_folder = async(basedir, appendto_element)=>{ let dirname = basedir; let folder_name = 'New Folder'; let newfolder_op_id = window.operation_id++; window.operation_cancelled[newfolder_op_id] = false; let newfolder_progress_window_init_ts = Date.now(); let progwin; // only show progress window if it takes longer than 500ms to create folder let progwin_timeout = setTimeout(async () => { progwin = await UIWindowProgress({ operation_id: newfolder_op_id, // TODO: Implement cancellation. // on_cancel: () => { // window.operation_cancelled[newfolder_op_id] = true; // }, }); progwin.set_status(i18n('taking_longer_than_usual')); }, 500); // create folder try{ await puter.fs.mkdir({ path: dirname + '/'+folder_name, rename: true, overwrite: false, success: function (data){ const el_created_dir = $(appendto_element).find('.item[data-path="'+html_encode(dirname)+'/'+html_encode(data.name)+'"]'); if(el_created_dir.length > 0){ window.activate_item_name_editor(el_created_dir); // Add action to actions_history for undo ability window.actions_history.push({ operation: 'create_folder', data: el_created_dir }); } clearTimeout(progwin_timeout); // done let newfolder_duration = (Date.now() - newfolder_progress_window_init_ts); if (progwin) { if (newfolder_duration >= window.copy_progress_hide_delay) { progwin.close(); } else { setTimeout(() => { setTimeout(() => { progwin.close(); }, Math.abs(window.copy_progress_hide_delay - newfolder_duration)); }); } } } }); }catch(err){ clearTimeout(progwin_timeout); } } window.create_file = async(options)=>{ // args let dirname = options.dirname; let appendto_element = options.append_to_element; let filename = options.name; let content = options.content ? [options.content] : []; // create file try{ puter.fs.upload(new File(content, filename), dirname, { success: async function (data){ const created_file = $(appendto_element).find('.item[data-path="'+html_encode(dirname)+'/'+html_encode(data.name)+'"]'); if(created_file.length > 0){ window.activate_item_name_editor(created_file); // Add action to actions_history for undo ability window.actions_history.push({ operation: 'create_file', data: created_file }); } } }); }catch(err){ console.log(err); } } window.create_shortcut = async(filename, is_dir, basedir, appendto_element, shortcut_to, shortcut_to_path)=>{ let dirname = basedir; const extname = path.extname(filename); const basename = path.basename(filename, extname) + ' - Shortcut'; filename = basename + extname; // create file shortcut try{ await puter.fs.upload(new File([], filename), dirname, { overwrite: false, shortcutTo: shortcut_to_path ?? shortcut_to, dedupeName: true, }); }catch(err){ console.log(err) } } window.copy_clipboard_items = async function(dest_path, dest_container_element){ let copy_op_id = window.operation_id++; window.operation_cancelled[copy_op_id] = false; // unselect previously selected items in the target container $(dest_container_element).children('.item-selected').removeClass('item-selected'); window.update_explorer_footer_selected_items_count($(dest_container_element).closest('.window')); let overwrite_all = false; (async()=>{ let copy_progress_window_init_ts = Date.now(); // only show progress window if it takes longer than 2s to copy let progwin; let progwin_timeout = setTimeout(async () => { progwin = await UIWindowProgress({ operation_id: copy_op_id, on_cancel: () => { window.operation_cancelled[copy_op_id] = true; }, }); }, 0); const copied_item_paths = [] for(let i=0; i${html_encode(err.entry_name)} already exists.`, buttons:[ {label: i18n('replace'), type: 'primary', value: 'replace'}, ... (window.clipboard.length > 1) ? [{label: i18n('replace_all'), value: 'replace_all'}] : [], ... (window.clipboard.length > 1) ? [{label: i18n('skip'), value: 'skip'}] : [{label: i18n('cancel'), value: 'cancel'}], ] }) if(alert_resp === 'replace'){ overwrite = true; }else if (alert_resp === 'replace_all'){ overwrite = true; overwrite_all = true; }else if(alert_resp === 'skip' || alert_resp === 'cancel'){ item_with_same_name_already_exists = false; } } else{ if(err.message){ UIAlert(err.message) } item_with_same_name_already_exists = false; } } }while(item_with_same_name_already_exists) } // done // Add action to actions_history for undo ability window.actions_history.push({ operation: 'copy', data: copied_item_paths }); clearTimeout(progwin_timeout); let copy_duration = (Date.now() - copy_progress_window_init_ts); if (progwin) { if (copy_duration >= window.copy_progress_hide_delay) { progwin.close(); } else { setTimeout(() => { setTimeout(() => { progwin.close(); }, Math.abs(window.copy_progress_hide_delay - copy_duration)); }); } } })(); } /** * Copies the given items to the destination path. * * @param {HTMLElement[]} el_items - HTML elements representing the items to copy * @param {string} dest_path - Destination path to copy items to */ window.copy_items = function(el_items, dest_path){ let copy_op_id = window.operation_id++; let overwrite_all = false; (async()=>{ let copy_progress_window_init_ts = Date.now(); // only show progress window if it takes longer than 2s to copy let progwin; let progwin_timeout = setTimeout(async () => { progwin = await UIWindowProgress({ operation_id: copy_op_id, on_cancel: () => { window.operation_cancelled[copy_op_id] = true; }, }); }, 2000); const copied_item_paths = [] for(let i=0; i < el_items.length; i++){ let copy_path = $(el_items[i]).attr('data-path'); let item_with_same_name_already_exists = true; let overwrite = overwrite_all; progwin?.set_status(i18n('copying_file', copy_path)); do{ if(overwrite) item_with_same_name_already_exists = false; // cancelled? if(window.operation_cancelled[copy_op_id]) return; try{ let resp = await puter.fs.copy({ source: copy_path, destination: dest_path, overwrite: overwrite || overwrite_all, // if user is copying an item to where the source is, automatically change the name so there is no conflict dedupeName: dest_path === path.dirname(copy_path), }) // remove overwritten item from the DOM if(resp[0].overwritten?.id){ $(`.item[data-uid=${resp.overwritten.id}]`).removeItems(); } // copy new path for undo copy copied_item_paths.push(resp[0].copied.path); // skips next loop iteration item_with_same_name_already_exists = false; }catch(err){ if(err.code === 'item_with_same_name_exists'){ const alert_resp = await UIAlert({ message: `${html_encode(err.entry_name)} already exists.`, buttons:[ { label: i18n('replace'), type: 'primary', value: 'replace' }, ... (el_items.length > 1) ? [{label: i18n('replace_all'), value: 'replace_all'}] : [], ... (el_items.length > 1) ? [{label: i18n('skip'), value: 'skip'}] : [{label: i18n('cancel'), value: 'cancel'}], ] }) if(alert_resp === 'replace'){ overwrite = true; }else if (alert_resp === 'replace_all'){ overwrite = true; overwrite_all = true; }else if(alert_resp === 'skip' || alert_resp === 'cancel'){ item_with_same_name_already_exists = false; } } else{ if(err.message){ UIAlert(err.message) } else if(err){ UIAlert(err) } item_with_same_name_already_exists = false; } } }while(item_with_same_name_already_exists) } // done // Add action to actions_history for undo ability window.actions_history.push({ operation: 'copy', data: copied_item_paths }); clearTimeout(progwin_timeout); let copy_duration = (Date.now() - copy_progress_window_init_ts); if (progwin) { if (copy_duration >= window.copy_progress_hide_delay) { progwin.close(); } else { setTimeout(() => { setTimeout(() => { progwin.close(); }, Math.abs(window.copy_progress_hide_delay - copy_duration)); }); } } })() } /** * Deletes the given item. * * @param {HTMLElement} el_item - HTML element representing the item to delete * @param {boolean} [descendants_only=false] - If true, only deletes descendant items under the given item * @returns {Promise} */ window.delete_item = async function(el_item, descendants_only = false){ if($(el_item).attr('data-immutable') === '1') return; // hide all UIItems with matching uids $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).fadeOut(150, function(){ // close all windows with matching uids $('.window-' + $(el_item).attr('data-uid')).close(); // close all windows that belong to a descendant of this item // todo this has to be case-insensitive but the `i` selector doesn't work on ^= $(`.window[data-path^="${$(el_item).attr('data-path')}/"]`).close(); }); try{ await puter.fs.delete({ paths: $(el_item).attr('data-path'), descendantsOnly: descendants_only, recursive: true, }); // fade out item $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).fadeOut(150, function(){ // find all parent windows that contain this item let parent_windows = $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).closest('.window'); // remove item from DOM $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).removeItems(); // update parent windows' item counts $(parent_windows).each(function(index){ window.update_explorer_footer_item_count(this); window.update_explorer_footer_selected_items_count(this); }); // update all shortcuts to this item $(`.item[data-shortcut_to_path="${html_encode($(el_item).attr('data-path'))}" i]`).attr(`data-shortcut_to_path`, ''); }); }catch(err){ UIAlert(err.responseText); } } window.move_clipboard_items = function (el_target_container, target_path){ let dest_path = target_path === undefined ? $(el_target_container).attr('data-path') : target_path; let el_items = []; if(window.clipboard.length > 0){ for(let i=0; i 0) window.move_items(el_items, dest_path); } window.clipboard = []; } /** * Initiates a download for multiple files provided as an array of paths. * * This function triggers the download of files from given paths. It constructs the * download URLs using an API base URL and the given paths, along with an authentication token. * Each file is then fetched and prompted to the user for download using the `saveAs` function. * * Global dependencies: * - `api_origin`: The base URL for the download API endpoint. * - `auth_token`: The authentication token required for the download API. * - `saveAs`: Function to save the fetched blob as a file. * - `path.basename()`: Function to extract the filename from the provided path. * * @global * @function trigger_download * @param {string[]} paths - An array of file paths that are to be downloaded. * * @example * let filePaths = ['/path/to/file1.txt', '/path/to/file2.png']; * window.trigger_download(filePaths); */ window.trigger_download = (paths)=>{ let urls = []; for (let index = 0; index < paths.length; index++) { urls.push({ download: window.api_origin + "/down?path=" + paths[index], filename: path.basename(paths[index]), }); } urls.forEach(async function (e) { const anti_csrf = await (async () => { const resp = await fetch(`${window.gui_origin}/get-anticsrf-token`); const { token } = await resp.json(); return token; })(); fetch(e.download, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + puter.authToken, }, body: JSON.stringify({ anti_csrf, }), }) .then(res => res.blob()) .then(blob => { saveAs(blob, e.filename); }); }); } /** * * @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', options.file_path ? `~/` + options.file_path.split('/').slice(1).join('/') : 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', options.file_path ? `~/` + options.file_path.split('/').slice(1).join('/') : undefined); 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'); const parent_win_id = $($el_parent_window).attr('data-id'); const is_dir = $(el_item).attr('data-is_dir') === '1' ? true : false; const uid = $(el_item).attr('data-shortcut_to') === '' ? $(el_item).attr('data-uid') : $(el_item).attr('data-shortcut_to'); const item_path = $(el_item).attr('data-shortcut_to_path') === '' ? $(el_item).attr('data-path') : $(el_item).attr('data-shortcut_to_path'); const is_shortcut = $(el_item).attr('data-is_shortcut') === '1'; const shortcut_to_path = $(el_item).attr('data-shortcut_to_path'); const associated_app_name = $(el_item).attr('data-associated_app_name'); const file_uid = $(el_item).attr('data-uid'); //---------------------------------------------------------------- // Is this a shortcut whose source is perma-deleted? //---------------------------------------------------------------- if(is_shortcut && shortcut_to_path === ''){ UIAlert(`This shortcut can't be opened because its source has been deleted.`) } //---------------------------------------------------------------- // Is this a shortcut whose source is trashed? //---------------------------------------------------------------- else if(is_shortcut && shortcut_to_path.startsWith(window.trash_path + '/')){ UIAlert(`This shortcut can't be opened because its source has been deleted.`) } //---------------------------------------------------------------- // Is this a trashed file? //---------------------------------------------------------------- else if(item_path.startsWith(window.trash_path + '/')){ UIAlert(`This item can't be opened because it's in the trash. To use this item, first drag it out of the Trash.`) } //---------------------------------------------------------------- // Is this a file (no dir) on a SaveFileDialog? //---------------------------------------------------------------- else if($el_parent_window.attr('data-is_saveFileDialog') === 'true' && !is_dir){ $el_parent_window.find('.savefiledialog-filename').val($(el_item).attr('data-name')); $el_parent_window.find('.savefiledialog-save-btn').trigger('click'); } //---------------------------------------------------------------- // Is this a file (no dir) on an OpenFileDialog? //---------------------------------------------------------------- else if($el_parent_window.attr('data-is_openFileDialog') === 'true' && !is_dir){ $el_parent_window.find('.window-disable-mask, .busy-indicator').show(); let busy_init_ts = Date.now(); try{ let filedialog_parent_uid = $el_parent_window.attr('data-parent_uuid'); let $filedialog_parent_app_window = $(`.window[data-element_uuid="${filedialog_parent_uid}"]`); let parent_window_app_uid = $filedialog_parent_app_window.attr('data-app_uuid'); const initiating_app_uuid = $el_parent_window.attr('data-initiating_app_uuid'); let res = await puter.fs.sign(window.host_app_uid ?? parent_window_app_uid, {uid: uid, action: 'write'}); res = res.items; // todo split is buggy because there might be a slash in the filename res.path = `~/` + item_path.split('/').slice(2).join('/'); const parent_uuid = $el_parent_window.attr('data-parent_uuid'); const return_to_parent_window = $el_parent_window.attr('data-return_to_parent_window') === 'true'; if(return_to_parent_window){ window.opener.postMessage({ msg: "fileOpenPicked", original_msg_id: $el_parent_window.attr('data-iframe_msg_uid'), items: Array.isArray(res) ? [...res] : [res], // LEGACY SUPPORT, remove this in the future when Polotno uses the new SDK // this is literally put in here to support Polotno's legacy code ...(!Array.isArray(res) && res) }, '*'); window.close(); } else if(parent_uuid){ // send event to iframe const target_iframe = $(`.window[data-element_uuid="${parent_uuid}"]`).find('.window-app-iframe').get(0); if(target_iframe){ let retobj = { msg: "fileOpenPicked", original_msg_id: $el_parent_window.attr('data-iframe_msg_uid'), items: Array.isArray(res) ? [...res] : [res], // LEGACY SUPPORT, remove this in the future when Polotno uses the new SDK // this is literally put in here to support Polotno's legacy code ...(!Array.isArray(res) && res) }; target_iframe.contentWindow.postMessage(retobj, '*'); } // focus iframe $(target_iframe).get(0)?.focus({preventScroll:true}); // send file_opened event const file_opened_event = new CustomEvent('file_opened', {detail: res}); // dispatch event to parent window $(`.window[data-element_uuid="${parent_uuid}"]`).get(0)?.dispatchEvent(file_opened_event); } }catch(e){ console.log(e); } // done let busy_duration = (Date.now() - busy_init_ts); if( busy_duration >= window.busy_indicator_hide_delay){ $el_parent_window.close(); }else{ setTimeout(() => { // close this dialog $el_parent_window.close(); }, Math.abs(window.busy_indicator_hide_delay - busy_duration)); } } //---------------------------------------------------------------- // 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({ name: window.user_preferences[`default_apps${path.extname(item_path).toLowerCase()}`], file_path: item_path, window_title: path.basename(item_path), maximized: options.maximized, file_uid: file_uid, }); } //---------------------------------------------------------------- // Is there an app associated with this item? //---------------------------------------------------------------- else if(associated_app_name !== ''){ window.launch_app({ name: associated_app_name, }) } //---------------------------------------------------------------- // Dir with no open windows: create a new window //---------------------------------------------------------------- else if(is_dir && ($el_parent_window.length === 0 || options.new_window)){ UIWindow({ path: item_path, title: path.basename(item_path), icon: await window.item_icon({is_dir: true, path: item_path}), uid: $(el_item).attr('data-uid'), is_dir: is_dir, app: 'explorer', 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, }); } //---------------------------------------------------------------- // Dir with an open window: change the path of the open window //---------------------------------------------------------------- else if($el_parent_window.length > 0 && is_dir){ window.window_nav_history[parent_win_id] = window.window_nav_history[parent_win_id].slice(0, window.window_nav_history_current_position[parent_win_id]+1); window.window_nav_history[parent_win_id].push(item_path); window.window_nav_history_current_position[parent_win_id]++; window.update_window_path($el_parent_window, item_path); } //---------------------------------------------------------------- // all other cases: try to open using an app //---------------------------------------------------------------- else{ const fspath = item_path.toLowerCase(); const fsuid = uid.toLowerCase(); let open_item_meta; // get all info needed to open an item try{ open_item_meta = await $.ajax({ url: window.api_origin + "/open_item", type: 'POST', contentType: "application/json", data: JSON.stringify({ uid: fsuid ?? undefined, path: fspath ?? undefined, }), headers: { "Authorization": "Bearer "+window.auth_token }, statusCode: { 401: function () { window.logout(); }, }, }); }catch(err){ // Ignored } // get a list of suggested apps for this file type. let suggested_apps = open_item_meta?.suggested_apps ?? await window.suggest_apps_for_fsentry({uid: fsuid, path: fspath}); //--------------------------------------------- // No suitable apps, ask if user would like to // download //--------------------------------------------- if(suggested_apps.length === 0){ //--------------------------------------------- // If .zip file, unzip it //--------------------------------------------- if(path.extname(item_path) === '.zip'){ window.unzipItem(item_path); return; } const alert_resp = await UIAlert( 'Found no suitable apps to open this file with. Would you like to download it instead?', [ { label: i18n('download_file'), value: 'download_file', type: 'primary', }, { label: i18n('cancel') } ]) if(alert_resp === 'download_file'){ window.trigger_download([item_path]); } return; } //--------------------------------------------- // First suggested app is default app to open this item //--------------------------------------------- else{ window.launch_app({ name: suggested_apps[0].name, token: open_item_meta.token, file_path: item_path, app_obj: suggested_apps[0], window_title: path.basename(item_path), file_uid: fsuid, maximized: options.maximized, file_signature: open_item_meta.signature, }); } } } /** * Moves the given items to the destination path. * * @param {HTMLElement[]} el_items - jQuery elements representing the items to move * @param {string} dest_path - The destination path to move the items to * @returns {Promise} */ window.move_items = async function(el_items, dest_path, is_undo = false){ let move_op_id = window.operation_id++; window.operation_cancelled[move_op_id] = false; // -------------------------------------------------------- // Optimization: in case all items being moved // are immutable do not proceed // -------------------------------------------------------- let all_items_are_immutable = true; for(let i=0; i { progwin = await UIWindowProgress({ operation_id: move_op_id, on_cancel: () => { window.operation_cancelled[move_op_id] = true; }, }); }, 2000); // storing moved items for undo ability const moved_items = [] // Go through each item and try to move it for(let i=0; iMoving ${html_encode($(el_item).attr('data-name'))}

Cannot move item to its current location.`) continue; } // if an item with the same name already exists in the destination path let item_with_same_name_already_exists = false; let overwrite = overwrite_all; let untrashed_at_least_one_item = false; // -------------------------------------------------------- // Keep trying to move the item until it succeeds or is cancelled // or user decides to overwrite or skip // -------------------------------------------------------- do{ try{ let path_to_show_on_progwin = $(el_item).attr('data-path'); // parse metadata if any let metadata = $(el_item).attr('data-metadata'); // no metadata? if(metadata === '' || metadata === 'null' || metadata === null) metadata = {} // try to parse metadata as JSON else{ try{ metadata = JSON.parse(metadata) }catch(e){ // Ignored } } let new_name; // user cancelled? if(window.operation_cancelled[move_op_id]) return; // indicates whether this is a recycling operation let recycling = false; let status_i18n_string = 'moving_file'; // -------------------------------------------------------- // Trashing // -------------------------------------------------------- if(dest_path === window.trash_path){ new_name = $(el_item).attr('data-uid'); metadata = { original_name: $(el_item).attr('data-name'), original_path: $(el_item).attr('data-path'), trashed_ts: Math.round(Date.now() / 1000), }; status_i18n_string = 'deleting_file'; // update other clients if(window.socket) window.socket.emit('trash.is_empty', {is_empty: false}); // change trash icons to 'trash-full.svg' $(`[data-app="trash"]`).find('.taskbar-icon > img').attr('src', window.icons['trash-full.svg']); $(`.item[data-path="${html_encode(window.trash_path)}" i], .item[data-shortcut_to_path="${html_encode(window.trash_path)}" i]`).find('.item-icon > img').attr('src', window.icons['trash-full.svg']); $(`.window[data-path="${html_encode(window.trash_path)}" i]`).find('.window-head-icon').attr('src', window.icons['trash-full.svg']); } // moving an item into a trashed directory? deny. else if(dest_path.startsWith(window.trash_path)){ progwin?.close(); UIAlert('Cannot move items into a deleted folder.'); return; } // -------------------------------------------------------- // If recycling an item, restore its original name // -------------------------------------------------------- else if(metadata.trashed_ts !== undefined){ recycling = true; new_name = metadata.original_name; metadata = {}; untrashed_at_least_one_item = true; path_to_show_on_progwin = window.trash_path + '/' + new_name; } // -------------------------------------------------------- // update progress window with current item being moved // -------------------------------------------------------- progwin?.set_status(i18n(status_i18n_string, path_to_show_on_progwin)); // execute move let resp = await puter.fs.move({ source: $(el_item).attr('data-uid'), destination: dest_path, overwrite: overwrite || overwrite_all, newName: new_name, // recycling requires making all missing dirs createMissingParents: recycling, newMetadata: metadata, excludeSocketID: window.socket?.id, }); let fsentry = resp.moved; // path must use the real name from DB fsentry.path = path.join(dest_path, fsentry.name); // skip next loop iteration because this iteration was successful item_with_same_name_already_exists = false; // update all shortcut_to_path $(`.item[data-shortcut_to_path="${html_encode($(el_item).attr('data-path'))}" i]`).attr(`data-shortcut_to_path`, fsentry.path); // Remove all items with matching uids $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).fadeOut(150, function(){ // find all parent windows that contain this item let parent_windows = $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).closest('.window'); // remove this item $(this).removeItems(); // update parent windows' item counts and selected item counts in their footers $(parent_windows).each(function(){ window.update_explorer_footer_item_count(this); window.update_explorer_footer_selected_items_count(this) }); }) // if trashing, close windows of trashed items and its descendants if(dest_path === window.trash_path){ $(`.window[data-path="${html_encode($(el_item).attr('data-path'))}" i]`).close(); // todo this has to be case-insensitive but the `i` selector doesn't work on ^= $(`.window[data-path^="${html_encode($(el_item).attr('data-path'))}/"]`).close(); } // update all paths of its and its descendants' open windows else{ // todo this has to be case-insensitive but the `i` selector doesn't work on ^= $(`.window[data-path^="${html_encode($(el_item).attr('data-path'))}/"], .window[data-path="${html_encode($(el_item).attr('data-path'))}" i]`).each(function(){ window.update_window_path(this, $(this).attr('data-path').replace($(el_item).attr('data-path'), path.join(dest_path, fsentry.name))); }) } if(dest_path === window.trash_path){ // if trashing dir... if($(el_item).attr('data-is_dir') === '1'){ // disassociate all its websites // todo, some client-side check to see if this dir has at least one associated website before sending ajax request // FIXME: dir_uuid is not defined, is this the same as the data-uid attribute? // puter.hosting.delete(dir_uuid) $(`.mywebsites-dir-path[data-uuid="${$(el_item).attr('data-uid')}"]`).remove(); // remove the website badge from all instances of the dir $(`.item[data-uid="${$(el_item).attr('data-uid')}"]`).find('.item-has-website-badge').fadeOut(300); } } // if replacing an existing item, remove the old item that was just replaced if(resp.overwritten?.id){ $(`.item[data-uid=${resp.overwritten.id}]`).removeItems(); } // if this is trash, get original name from item metadata fsentry.name = metadata?.original_name || fsentry.name; // create new item on matching containers const options = { appendTo: $(`.item-container[data-path="${html_encode(dest_path)}" i]`), immutable: fsentry.immutable, associated_app_name: fsentry.associated_app?.name, uid: fsentry.uid, path: fsentry.path, icon: await window.item_icon(fsentry), name: (dest_path === window.trash_path) ? $(el_item).attr('data-name') : fsentry.name, is_dir: fsentry.is_dir, size: fsentry.size, type: fsentry.type, modified: fsentry.modified, is_selected: false, is_shared: (dest_path === window.trash_path) ? false : fsentry.is_shared, is_shortcut: fsentry.is_shortcut, shortcut_to: fsentry.shortcut_to, shortcut_to_path: fsentry.shortcut_to_path, has_website: $(el_item).attr('data-has_website') === '1', metadata: fsentry.metadata ?? '', suggested_apps: fsentry.suggested_apps, } UIItem(options); moved_items.push({'options': options, 'original_path': $(el_item).attr('data-path')}); // this operation may have created some missing directories, // see if any of the directories in the path of this file is new AND // if these new path have any open parents that need to be updated resp.parent_dirs_created?.forEach(async dir => { let item_container = $(`.item-container[data-path="${html_encode(path.dirname(dir.path))}" i]`); if(item_container.length > 0 && $(`.item[data-path="${html_encode(dir.path)}" i]`).length === 0){ UIItem({ appendTo: item_container, immutable: false, uid: dir.uid, path: dir.path, icon: await window.item_icon(dir), name: dir.name, size: dir.size, type: dir.type, modified: dir.modified, is_dir: true, is_selected: false, is_shared: dir.is_shared, has_website: false, suggested_apps: dir.suggested_apps, }); } window.sort_items(item_container); }); //sort each container $(`.item-container[data-path="${html_encode(dest_path)}" i]`).each(function(){ window.sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order')) }) }catch(err){ // ----------------------------------------------------------------------- // if item with same name already exists, ask user if they want to overwrite // ----------------------------------------------------------------------- if(err.code==='item_with_same_name_exists'){ item_with_same_name_already_exists = true; const alert_resp = await UIAlert({ message: `${html_encode(err.entry_name)} already exists.`, buttons:[ { label: i18n('replace'), type: 'primary', value: 'replace' }, ... (el_items.length > 1) ? [{label: i18n('replace_all'), value: 'replace_all'}] : [], ... (el_items.length > 1) ? [{label: i18n('skip'), value: 'skip'}] : [{label: i18n('cancel'), value: 'cancel'}], ] }) if(alert_resp === 'replace'){ overwrite = true; }else if (alert_resp === 'replace_all'){ overwrite = true; overwrite_all = true; }else if(alert_resp === 'skip' || alert_resp === 'cancel'){ item_with_same_name_already_exists = false; } } // ----------------------------------------------------------------------- // all other errors // ----------------------------------------------------------------------- else{ item_with_same_name_already_exists = false; // error message after source item has reappeared $(el_item).show(0, function(){ UIAlert(`

Moving ${html_encode($(el_item).attr('data-name'))}

${err.message ?? ''}`) }); break; } } }while(item_with_same_name_already_exists); // check if trash is empty if(untrashed_at_least_one_item){ const trash = await puter.fs.stat(window.trash_path); if(window.socket){ window.socket.emit('trash.is_empty', {is_empty: trash.is_empty}); } if(trash.is_empty){ $(`[data-app="trash"]`).find('.taskbar-icon > img').attr('src', window.icons['trash.svg']); $(`.item[data-path="${html_encode(window.trash_path)}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']); $(`.window[data-path="${html_encode(window.trash_path)}" i]`).find('.window-head-icon').attr('src', window.icons['trash.svg']); } } } clearTimeout(progwin_timeout); // log stats to console let move_duration = (Date.now() - move_init_ts); console.log(`moved ${el_items.length} item${el_items.length > 1 ? 's':''} in ${move_duration}ms`); // ----------------------------------------------------------------------- // DONE! close progress window with delay to allow user to see 100% progress // ----------------------------------------------------------------------- // Add action to actions_history for undo ability if(!is_undo && dest_path !== window.trash_path){ window.actions_history.push({ operation: 'move', data: moved_items, }); }else if(!is_undo && dest_path === window.trash_path){ window.actions_history.push({ operation: 'delete', data: moved_items, }); } if(progwin){ setTimeout(() => { progwin.close(); }, window.copy_progress_hide_delay); } } /** * Generates sharing URLs for various social media platforms and services based on the provided arguments. * * @global * @function * @param {Object} args - Configuration object for generating share URLs. * @param {string} [args.url] - The URL to share. * @param {string} [args.title] - The title or headline of the content to share. * @param {string} [args.image] - Image URL associated with the content. * @param {string} [args.desc] - A description of the content. * @param {string} [args.appid] - App ID for certain platforms that require it. * @param {string} [args.redirecturl] - Redirect URL for certain platforms. * @param {string} [args.via] - Attribution source, e.g., a Twitter username. * @param {string} [args.hashtags] - Comma-separated list of hashtags without '#'. * @param {string} [args.provider] - Content provider. * @param {string} [args.language] - Content's language. * @param {string} [args.userid] - User ID for certain platforms. * @param {string} [args.category] - Content's category. * @param {string} [args.phonenumber] - Phone number for platforms like SMS or Telegram. * @param {string} [args.emailaddress] - Email address to share content to. * @param {string} [args.ccemailaddress] - CC email address for sharing content. * @param {string} [args.bccemailaddress] - BCC email address for sharing content. * @returns {Object} - An object containing key-value pairs where keys are platform names and values are constructed sharing URLs. * * @example * const shareConfig = { * url: 'https://example.com', * title: 'Check this out!', * desc: 'This is an amazing article on example.com', * via: 'exampleUser' * }; * const shareLinks = window.socialLink(shareConfig); * console.log(shareLinks.twitter); // Outputs the constructed Twitter share link */ window.socialLink = function (args) { const validargs = [ 'url', 'title', 'image', 'desc', 'appid', 'redirecturl', 'via', 'hashtags', 'provider', 'language', 'userid', 'category', 'phonenumber', 'emailaddress', 'cemailaddress', 'bccemailaddress', ]; for(var i = 0; i < validargs.length; i++) { const validarg = validargs[i]; if(!args[validarg]) { args[validarg] = ''; } } const url = fixedEncodeURIComponent(args.url); const title = fixedEncodeURIComponent(args.title); const image = fixedEncodeURIComponent(args.image); const desc = fixedEncodeURIComponent(args.desc); const via = fixedEncodeURIComponent(args.via); const hash_tags = fixedEncodeURIComponent(args.hashtags); const language = fixedEncodeURIComponent(args.language); const user_id = fixedEncodeURIComponent(args.userid); const category = fixedEncodeURIComponent(args.category); const phone_number = fixedEncodeURIComponent(args.phonenumber); const email_address = fixedEncodeURIComponent(args.emailaddress); const cc_email_address = fixedEncodeURIComponent(args.ccemailaddress); const bcc_email_address = fixedEncodeURIComponent(args.bccemailaddress); var text = title; if(desc) { text += '%20%3A%20'; // This is just this, " : " text += desc; } return { 'add.this':'http://www.addthis.com/bookmark.php?url=' + url, 'blogger':'https://www.blogger.com/blog-this.g?u=' + url + '&n=' + title + '&t=' + desc, 'buffer':'https://buffer.com/add?text=' + text + '&url=' + url, 'diaspora':'https://share.diasporafoundation.org/?title=' + title + '&url=' + url, 'douban':'http://www.douban.com/recommend/?url=' + url + '&title=' + text, 'email':'mailto:' + email_address + '?subject=' + title + '&body=' + desc, 'evernote':'https://www.evernote.com/clip.action?url=' + url + '&title=' + text, 'getpocket':'https://getpocket.com/edit?url=' + url, 'facebook':'http://www.facebook.com/sharer.php?u=' + url, 'flattr':'https://flattr.com/submit/auto?user_id=' + user_id + '&url=' + url + '&title=' + title + '&description=' + text + '&language=' + language + '&tags=' + hash_tags + '&hidden=HIDDEN&category=' + category, 'flipboard':'https://share.flipboard.com/bookmarklet/popout?v=2&title=' + text + '&url=' + url, 'gmail':'https://mail.google.com/mail/?view=cm&to=' + email_address + '&su=' + title + '&body=' + url + '&bcc=' + bcc_email_address + '&cc=' + cc_email_address, 'google.bookmarks':'https://www.google.com/bookmarks/mark?op=edit&bkmk=' + url + '&title=' + title + '&annotation=' + text + '&labels=' + hash_tags + '', 'instapaper':'http://www.instapaper.com/edit?url=' + url + '&title=' + title + '&description=' + desc, 'line.me':'https://lineit.line.me/share/ui?url=' + url + '&text=' + text, 'linkedin':'https://www.linkedin.com/sharing/share-offsite/?url=' + url, 'livejournal':'http://www.livejournal.com/update.bml?subject=' + text + '&event=' + url, 'hacker.news':'https://news.ycombinator.com/submitlink?u=' + url + '&t=' + title, 'ok.ru':'https://connect.ok.ru/dk?st.cmd=WidgetSharePreview&st.shareUrl=' + url, 'pinterest':'http://pinterest.com/pin/create/button/?url=' + url , 'qzone':'http://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=' + url, 'reddit':'https://reddit.com/submit?url=' + url + '&title=' + title, 'renren':'http://widget.renren.com/dialog/share?resourceUrl=' + url + '&srcUrl=' + url + '&title=' + text + '&description=' + desc, 'skype':'https://web.skype.com/share?url=' + url + '&text=' + text, 'sms':'sms:' + phone_number + '?body=' + text, 'surfingbird.ru':'http://surfingbird.ru/share?url=' + url + '&description=' + desc + '&screenshot=' + image + '&title=' + title, 'telegram.me':'https://t.me/share/url?url=' + url + '&text=' + text + '&to=' + phone_number, 'threema':'threema://compose?text=' + text + '&id=' + user_id, 'tumblr':'https://www.tumblr.com/widgets/share/tool?canonicalUrl=' + url + '&title=' + title + '&caption=' + desc + '&tags=' + hash_tags, 'twitter':'https://twitter.com/intent/tweet?url=' + url + '&text=' + text + '&via=' + via + '&hashtags=' + hash_tags, 'vk':'http://vk.com/share.php?url=' + url + '&title=' + title + '&comment=' + desc, 'weibo':'http://service.weibo.com/share/share.php?url=' + url + '&appkey=&title=' + title + '&pic=&ralateUid=', 'whatsapp':'https://api.whatsapp.com/send?text=' + text + '%20' + url, 'xing':'https://www.xing.com/spi/shares/new?url=' + url, 'yahoo':'http://compose.mail.yahoo.com/?to=' + email_address + '&subject=' + title + '&body=' + text, }; } /** * Encodes a URI component with enhanced safety by replacing characters * that are not typically encoded by the standard encodeURIComponent. * * @param {string} str - The string to be URI encoded. * @returns {string} - Returns the URI encoded string. * * @example * const str = "Hello, world!"; * const encodedStr = fixedEncodeURIComponent(str); * console.log(encodedStr); // Expected output: "Hello%2C%20world%21" */ function fixedEncodeURIComponent(str) { return encodeURIComponent(str).replace(/[!'()*]/g, function(c) { return '%' + c.charCodeAt(0).toString(16); }); } /** * Refreshes the desktop background based on the user's settings. * If the user has set a custom desktop background URL or color, it will use that. * If not, it defaults to a specific wallpaper image. * * @global * @function * @fires set_desktop_background - Calls this global function to set the desktop background. * * @example * // This will refresh the desktop background according to the user's preference or defaults. * window.refresh_desktop_background(); */ window.refresh_desktop_background = function(){ if(window.user && (window.user.desktop_bg_url !== null || window.user.desktop_bg_color !== null)){ window.set_desktop_background({ url: window.user.desktop_bg_url, fit: window.user.desktop_bg_fit, color: window.user.desktop_bg_color, }) } // default background else{ let wallpaper = (window.gui_env === 'prod') ? '/dist/images/wallpaper.webp' : '/src/images/wallpaper.webp'; window.set_desktop_background({ url: wallpaper, fit: 'cover', }); } } window.determine_website_url = function(fsentry_path){ // search window.sites and if any site has `dir_path` set and the fsentry_path starts with that dir_path + '/', return the site's url + path for(let i=0; i{ if(sites && sites.length > 0){ window.sites = sites; }else{ window.sites = []; } }) } /** * * @param {*} el_target_container * @param {*} target_path */ window.init_upload_using_dialog = function(el_target_container, target_path = null){ $("#upload-file-dialog").unbind('onchange'); $("#upload-file-dialog").unbind('change'); $("#upload-file-dialog").unbind('onChange'); target_path = target_path === null ? $(el_target_container).attr('data-path') : path.resolve(target_path); $('#upload-file-dialog').trigger('click'); $("#upload-file-dialog").on('change', async function(e){ if($("#upload-file-dialog").val() !== ''){ const files = $('#upload-file-dialog')[0].files; if(files.length > 0){ try{ window.upload_items(files, target_path); } catch(err){ UIAlert(err.message ?? err) } $('#upload-file-dialog').val(''); } } else{ return } }) } window.upload_items = async function(items, dest_path){ let upload_progress_window; let opid; if(dest_path == window.trash_path){ UIAlert('Uploading to trash is not allowed!'); return; } puter.fs.upload( // what to upload items, // where to upload dest_path, // options { // init init: async(operation_id, xhr)=>{ opid = operation_id; // create upload progress window upload_progress_window = await UIWindowProgress({ title: i18n('upload'), icon: window.icons[`app-icon-uploader.svg`], operation_id: operation_id, show_progress: true, on_cancel: () => { window.show_save_account_notice_if_needed(); xhr.abort(); }, }); // add to active_uploads window.active_uploads[opid] = 0; }, // start start: async function(){ // change upload progress window message to uploading upload_progress_window.set_status('Uploading'); upload_progress_window.set_progress(0); }, // progress progress: async function(operation_id, op_progress){ upload_progress_window.set_progress(op_progress); // update active_uploads window.active_uploads[opid] = op_progress; // update title if window is not visible if(document.visibilityState !== "visible"){ update_title_based_on_uploads(); } }, // success success: async function(items){ // DONE // Add action to actions_history for undo ability const files = [] if(typeof items[Symbol.iterator] === 'function'){ for(const item of items){ files.push(item.path) } }else{ files.push(items.path) } window.actions_history.push({ operation: 'upload', data: files }); // close progress window after a bit of delay for a better UX setTimeout(() => { setTimeout(() => { upload_progress_window.close(); window.show_save_account_notice_if_needed(); }, Math.abs(window.upload_progress_hide_delay)); }) // remove from active_uploads delete window.active_uploads[opid]; }, // error error: async function(err){ upload_progress_window.show_error(i18n('error_uploading_files'), err.message); // remove from active_uploads delete window.active_uploads[opid]; }, // abort abort: async function(operation_id){ // console.log('upload aborted'); // remove from active_uploads delete window.active_uploads[opid]; } } ); } window.empty_trash = async function(){ const alert_resp = await UIAlert({ message: i18n('empty_trash_confirmation'), buttons:[ { label: i18n('yes'), value: 'yes', type: 'primary', }, { label: i18n('no'), value: 'no', }, ] }) if(alert_resp === 'no') return; // only show progress window if it takes longer than 500ms to create folder let init_ts = Date.now(); let progwin; let op_id = window.uuidv4(); let progwin_timeout = setTimeout(async () => { progwin = await UIWindowProgress({operation_id: op_id}); progwin.set_status(i18n('emptying_trash')); }, 500); await puter.fs.delete({ paths: window.trash_path, descendantsOnly: true, recursive: true, success: async function (resp){ // update other clients if(window.socket){ window.socket.emit('trash.is_empty', {is_empty: true}); } // use the 'empty trash' icon for Trash $(`[data-app="trash"]`).find('.taskbar-icon > img').attr('src', window.icons['trash.svg']); $(`.item[data-path="${html_encode(window.trash_path)}" i], .item[data-shortcut_to_path="${html_encode(window.trash_path)}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']); $(`.window[data-path="${window.trash_path}"]`).find('.window-head-icon').attr('src', window.icons['trash.svg']); // remove all items with trash paths // todo this has to be case-insensitive but the `i` selector doesn't work on ^= $(`.item[data-path^="${window.trash_path}/"]`).removeItems(); // update the footer item count for Trash window. update_explorer_footer_item_count($(`.window[data-path="${window.trash_path}"]`)) // close progress window clearTimeout(progwin_timeout); setTimeout(() => { progwin?.close(); }, Math.max(0, window.copy_progress_hide_delay - (Date.now() - init_ts))); }, error: async function (err){ clearTimeout(progwin_timeout); setTimeout(() => { progwin?.close(); }, Math.max(0, window.copy_progress_hide_delay - (Date.now() - init_ts))); } }); } window.copy_to_clipboard = async function(text){ if (navigator.clipboard) { // copy text to clipboard await navigator.clipboard.writeText(text); } else{ document.execCommand('copy'); } } window.getUsage = () => { return fetch(window.api_origin + "/drivers/usage", { headers: { "Content-Type": "application/json", "Authorization": "Bearer " + window.auth_token }, method: "GET" }) .then(response => { // Check if the response is ok (status code in the range 200-299) if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); // Parse the response as JSON }) .then(data => { // Handle the JSON data return data; }) .catch(error => { // Handle any errors console.error('There has been a problem with your fetch operation:', error); }); } window.getAppUIDFromOrigin = async function(origin) { try { const response = await fetch(window.api_origin + "/auth/app-uid-from-origin", { headers: { "Content-Type": "application/json", "Authorization": "Bearer " + window.auth_token, }, body: JSON.stringify({ origin: origin }), method: "POST", }); const data = await response.json(); // Assuming the app_uid is in the data object, return it return data.uid; } catch (err) { // Handle any errors here console.error(err); // You may choose to return something specific here in case of an error return null; } } window.getUserAppToken = async function(origin) { try { const response = await fetch(window.api_origin + "/auth/get-user-app-token", { headers: { "Content-Type": "application/json", "Authorization": "Bearer " + window.auth_token, }, body: JSON.stringify({ origin: origin }), method: "POST", }); const data = await response.json(); // return return data; } catch (err) { // Handle any errors here console.error(err); // You may choose to return something specific here in case of an error return null; } } window.checkUserSiteRelationship = async function(origin) { try { const response = await fetch(window.api_origin + "/auth/check-app ", { headers: { "Content-Type": "application/json", "Authorization": "Bearer " + window.auth_token, }, body: JSON.stringify({ origin: origin }), method: "POST", }); const data = await response.json(); // return return data; } catch (err) { // Handle any errors here console.error(err); // You may choose to return something specific here in case of an error return null; } } window.zipItems = async function(el_items, targetDirPath, download = true) { const zip = new JSZip(); // if single item, convert to array el_items = Array.isArray(el_items) ? el_items : [el_items]; // create progress window let start_ts = Date.now(); let progwin, progwin_timeout; // only show progress window if it takes longer than 500ms to download progwin_timeout = setTimeout(async () => { progwin = await UIWindowProgress(); }, 500); for (const el_item of el_items) { let targetPath = $(el_item).attr('data-path'); // if directory, zip the directory if($(el_item).attr('data-is_dir') === '1'){ progwin?.set_status(i18n('reading_file', targetPath)); // Recursively read the directory let children = await readDirectoryRecursive(targetPath); // Add files to the zip for (const child of children) { let relativePath; if(el_items.length === 1) relativePath = child.relativePath; else relativePath = path.basename(targetPath) + '/' + child.relativePath; // update progress window progwin?.set_status(i18n('zipping_file', relativePath)); // read file content let content = await puter.fs.read(child.path); try{ zip.file(relativePath, content, {binary: true}); }catch(e){ console.error(e); } } } // if item is a file, zip the file else{ let content = await puter.fs.read(targetPath); zip.file(path.basename(targetPath), content, {binary: true}); } } // determine name of zip file let zipName; if(el_items.length === 1) zipName = path.basename($(el_items[0]).attr('data-path')); else zipName = 'Archive'; // Generate the zip file zip.generateAsync({ type: "blob" }) .then(async function (content) { // Trigger the download if(download){ const url = URL.createObjectURL(content); const a = document.createElement("a"); a.href = url; a.download = zipName; document.body.appendChild(a); a.click(); // Cleanup document.body.removeChild(a); URL.revokeObjectURL(url); } // save else await puter.fs.write(targetDirPath + '/' + zipName + ".zip", content, {overwrite: false, dedupeName: true}) // close progress window clearTimeout(progwin_timeout); setTimeout(() => { progwin?.close(); }, Math.max(0, window.copy_progress_hide_delay - (Date.now() - start_ts))); }) .catch(function (err) { // close progress window clearTimeout(progwin_timeout); setTimeout(() => { progwin?.close(); }, Math.max(0, window.copy_progress_hide_delay - (Date.now() - start_ts))); // handle errors // TODO: Display in progress dialog console.error("Error in zipping files: ", err); }); } async function readDirectoryRecursive(path, baseDir = '') { let allFiles = []; // Read the directory const entries = await puter.fs.readdir(path); // Process each entry for (const entry of entries) { const fullPath = `${path}/${entry.name}`; if (entry.is_dir) { // If entry is a directory, recursively read it const subDirFiles = await readDirectoryRecursive(fullPath, `${baseDir}${entry.name}/`); allFiles = allFiles.concat(subDirFiles); } else { // If entry is a file, add it to the list allFiles.push({ path: fullPath, relativePath: `${baseDir}${entry.name}` }); } } return allFiles; } window.extractSubdomain = function(url) { var subdomain = url.split('://')[1].split('.')[0]; return subdomain; } window.sleep = function(ms){ return new Promise(resolve => setTimeout(resolve, ms)); } window.unzipItem = async function(itemPath) { // create progress window let start_ts = Date.now(); let progwin, progwin_timeout; // only show progress window if it takes longer than 500ms to download progwin_timeout = setTimeout(async () => { progwin = await UIWindowProgress(); }, 500); const zip = new JSZip(); let filPath = itemPath; let file = puter.fs.read(filPath); zip.loadAsync(file).then(async function (zip) { const rootdir = await puter.fs.mkdir(path.dirname(filPath) + '/' + path.basename(filPath, '.zip'), {dedupeName: true}); Object.keys(zip.files).forEach(async function (filename) { console.log(filename); if(filename.endsWith('/')) await puter.fs.mkdir(rootdir.path +'/' + filename, {createMissingParents: true}); zip.files[filename].async('blob').then(async function (fileData) { await puter.fs.write(rootdir.path +'/' + filename, fileData); }).catch(function (e) { // UIAlert(e.message); }) }) // close progress window clearTimeout(progwin_timeout); setTimeout(() => { progwin?.close(); }, Math.max(0, window.copy_progress_hide_delay - (Date.now() - start_ts))); }).catch(function (e) { // UIAlert(e.message); // close progress window clearTimeout(progwin_timeout); setTimeout(() => { progwin?.close(); }, Math.max(0, window.copy_progress_hide_delay - (Date.now() - start_ts))); }) } window.rename_file = async(options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url, is_undo = false)=>{ puter.fs.rename({ uid: options.uid === 'null' ? null : options.uid, new_name: new_name, excludeSocketID: window.socket.id, success: async (fsentry)=>{ // Add action to actions_history for undo ability if (!is_undo) window.actions_history.push({ operation: 'rename', data: {options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url} }); // Has the extension changed? in that case update options.sugggested_apps const old_extension = path.extname(old_name); const new_extension = path.extname(new_name); if(old_extension !== new_extension){ window.suggest_apps_for_fsentry({ uid: options.uid, onSuccess: function(suggested_apps){ options.suggested_apps = suggested_apps; } }); } // Set new item name $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name`).html(html_encode(truncate_filename(new_name)).replaceAll(' ', ' ')); $(el_item_name).show(); // Hide item name editor $(el_item_name_editor).hide(); // Set new icon const new_icon = (options.is_dir ? window.icons['folder.svg'] : (await window.item_icon(fsentry)).image); $(el_item_icon).find('.item-icon-icon').attr('src', new_icon); // Set new data-name options.name = new_name; $(el_item).attr('data-name', html_encode(new_name)); $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-name', html_encode(new_name)); $(`.window-${options.uid}`).attr('data-name', html_encode(new_name)); // Set new title attribute $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('title', html_encode(new_name)); $(`.window-${options.uid}`).attr('title', html_encode(new_name)); // Set new value for item-name-editor $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name-editor`).val(html_encode(new_name)); $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name`).attr('title', html_encode(new_name)); // Set new data-path options.path = path.join( path.dirname(options.path), options.name); const new_path = options.path; $(el_item).attr('data-path', new_path); $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-path', new_path); $(`.window-${options.uid}`).attr('data-path', new_path); // Update all elements that have matching paths $(`[data-path="${html_encode(old_path)}" i]`).each(function(){ $(this).attr('data-path', new_path) if($(this).hasClass('window-navbar-path-dirname')) $(this).text(new_name); }); // Update the paths of all elements whose paths start with old_path $(`[data-path^="${html_encode(old_path) + '/'}"]`).each(function(){ const new_el_path = _.replace($(this).attr('data-path'), old_path + '/', new_path+'/'); $(this).attr('data-path', new_el_path); }); // Update the 'Sites Cache' if($(el_item).attr('data-has_website') === '1') await window.update_sites_cache(); // Update website_url website_url = window.determine_website_url(new_path); $(el_item).attr('data-website_url', website_url); // Update all exact-matching windows $(`.window-${options.uid}`).each(function(){ window.update_window_path(this, options.path); }) // Set new name for corresponding open windows $(`.window-${options.uid} .window-head-title`).text(new_name); // Re-sort all matching item containers $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).parent('.item-container').each(function(){ window.sort_items(this, $(el_item).closest('.item-container').attr('data-sort_by'), $(el_item).closest('.item-container').attr('data-sort_order')); }) }, error: function (err){ // reset to old name $(el_item_name).text(truncate_filename(options.name)); $(el_item_name).show(); // hide item name editor $(el_item_name_editor).hide(); $(el_item_name_editor).val(html_encode($(el_item).attr('data-name'))); //show error if(err.message){ UIAlert(err.message) } }, }); } /** * Deletes the given item with path. * * @param {string} path - path of the item to delete * @returns {Promise} */ window.delete_item_with_path = async function(path){ try{ await puter.fs.delete({ paths: path, descendantsOnly: false, recursive: true, }); }catch(err){ UIAlert(err.responseText); } } window.undo_last_action = async()=>{ if (window.actions_history.length > 0) { const last_action = window.actions_history.pop(); // Undo the create file action if (last_action.operation === 'create_file' || last_action.operation === 'create_folder') { const lastCreatedItem = last_action.data; window.undo_create_file_or_folder(lastCreatedItem); } else if(last_action.operation === 'rename') { const {options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url} = last_action.data; window.rename_file(options, old_name, new_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url, true); } else if(last_action.operation === 'upload') { const files = last_action.data; window.undo_upload(files); } else if(last_action.operation === 'copy') { const files = last_action.data; window.undo_copy(files); } else if(last_action.operation === 'move') { const items = last_action.data; window.undo_move(items); } else if(last_action.operation === 'delete') { const items = last_action.data; window.undo_delete(items); } } } window.undo_create_file_or_folder = async(item)=>{ await window.delete_item(item); } window.undo_upload = async(files)=>{ for (const file of files) { await window.delete_item_with_path(file); } } window.undo_copy = async(files)=>{ for (const file of files) { await window.delete_item_with_path(file); } } window.undo_move = async(items)=>{ for (const item of items) { const el = await window.get_html_element_from_options(item.options); console.log(item.original_path) window.move_items([el], path.dirname(item.original_path), true); } } window.undo_delete = async(items)=>{ for (const item of items) { const el = await window.get_html_element_from_options(item.options); let metadata = $(el).attr('data-metadata') === '' ? {} : JSON.parse($(el).attr('data-metadata')) window.move_items([el], path.dirname(metadata.original_path), true); } } window.get_html_element_from_options = async function(options){ const item_id = window.global_element_id++; options.disabled = options.disabled ?? false; options.visible = options.visible ?? 'visible'; // one of 'visible', 'revealed', 'hidden' options.is_dir = options.is_dir ?? false; options.is_selected = options.is_selected ?? false; options.is_shared = options.is_shared ?? false; options.is_shortcut = options.is_shortcut ?? 0; options.is_trash = options.is_trash ?? false; options.metadata = options.metadata ?? ''; options.multiselectable = options.multiselectable ?? true; options.shortcut_to = options.shortcut_to ?? ''; options.shortcut_to_path = options.shortcut_to_path ?? ''; options.immutable = (options.immutable === false || options.immutable === 0 || options.immutable === undefined ? 0 : 1); options.sort_container_after_append = (options.sort_container_after_append !== undefined ? options.sort_container_after_append : false); const is_shared_with_me = (options.path !== '/'+window.user.username && !options.path.startsWith('/'+window.user.username+'/')); let website_url = window.determine_website_url(options.path); // do a quick check to see if the target parent has any file type restrictions const appendto_allowed_file_types = $(options.appendTo).attr('data-allowed_file_types') if(!window.check_fsentry_against_allowed_file_types_string({is_dir: options.is_dir, name:options.name, type:options.type}, appendto_allowed_file_types)) options.disabled = true; // -------------------------------------------------------- // HTML for Item // -------------------------------------------------------- let h = ''; h += `
`; // spinner h += `
`; h += `
`; // modified h += `
`; h += `${options.modified === 0 ? '-' : timeago.format(options.modified*1000)}`; h += `
`; // size h += `
`; h += `${options.size ? window.byte_format(options.size) : '-'}`; h += `
`; // type h += `
`; if(options.is_dir) h += `Folder`; else h += `${options.type ? html_encode(options.type) : '-'}`; h += `
`; // icon h += `
`; h += ``; h += `
`; // badges h += `
`; // website badge h += ``; // link badge h += ``; // shared badge h += ``; // owner-shared badge h += ``; // shortcut badge h += ``; h += `
`; // name h += `${html_encode(truncate_filename(options.name)).replaceAll(' ', ' ')}` // name editor h += `` h += `
`; return h; } window.store_auto_arrange_preference = (preference)=>{ puter.kv.set('user_preferences.auto_arrange_desktop', preference); localStorage.setItem('auto_arrange', preference); } window.get_auto_arrange_data = async()=>{ const preferenceValue = await puter.kv.get('user_preferences.auto_arrange_desktop'); window.is_auto_arrange_enabled = preferenceValue === null ? true : preferenceValue; const positions = await puter.kv.get('desktop_item_positions') window.desktop_item_positions = (!positions || typeof positions !== 'object' || Array.isArray(positions)) ? {} : positions; } window.clear_desktop_item_positions = async(el_desktop)=>{ $(el_desktop).find('.item').each(function(){ const el_item = $(this)[0]; $(el_item).css('position', ''); $(el_item).css('left', ''); $(el_item).css('top', ''); }); if(window.reset_item_positions){ window.delete_desktop_item_positions() } } window.set_desktop_item_positions = async(el_desktop)=>{ $(el_desktop).find('.item').each(async function(){ const position = window.desktop_item_positions[$(this).attr('data-uid')] const el_item = $(this)[0]; if(position){ $(el_item).css('position', 'absolute'); $(el_item).css('left', position.left + 'px'); $(el_item).css('top', position.top + 'px'); } }); } window.save_desktop_item_positions = ()=>{ puter.kv.set('desktop_item_positions', window.desktop_item_positions); } window.delete_desktop_item_positions = ()=>{ window.desktop_item_positions = {} puter.kv.del('desktop_item_positions'); } window.change_clock_visible = (clock_visible) => { let newValue = clock_visible || window.user_preferences.clock_visible; newValue === 'auto' && window.is_fullscreen() ? $('#clock').show() : $('#clock').hide(); newValue === 'show' && $('#clock').show(); newValue === 'hide' && $('#clock').hide(); if(clock_visible) { // save clock_visible to user preferences window.mutate_user_preferences({ clock_visible: newValue }); return; } $('select.change-clock-visible').val(window.user_preferences.clock_visible); } // Finds the `.window` element for the given app instance ID window.window_for_app_instance = (instance_id) => { return $(`.window[data-element_uuid="${instance_id}"]`).get(0); }; // Finds the `iframe` element for the given app instance ID window.iframe_for_app_instance = (instance_id) => { return $(window.window_for_app_instance(instance_id)).find('.window-app-iframe').get(0); }; // Run any callbacks to say that the app has launched window.report_app_launched = (instance_id, { uses_sdk = true }) => { const child_launch_callback = window.child_launch_callbacks[instance_id]; if (child_launch_callback) { const parent_iframe = window.iframe_for_app_instance(child_launch_callback.parent_instance_id); // send confirmation to requester window parent_iframe.contentWindow.postMessage({ msg: 'childAppLaunched', original_msg_id: child_launch_callback.launch_msg_id, child_instance_id: instance_id, uses_sdk: uses_sdk, }, '*'); delete window.child_launch_callbacks[instance_id]; } }; // Run any callbacks to say that the app has closed window.report_app_closed = (instance_id, status_code) => { const el_window = window.window_for_app_instance(instance_id); // notify parent app, if we have one, that we're closing const parent_id = el_window.dataset['parent_instance_id']; const parent = $(`.window[data-element_uuid="${parent_id}"] .window-app-iframe`).get(0); if (parent) { parent.contentWindow.postMessage({ msg: 'appClosed', appInstanceID: instance_id, statusCode: status_code ?? 0, }, '*'); } // notify child apps, if we have them, that we're closing const children = $(`.window[data-parent_instance_id="${instance_id}"] .window-app-iframe`); children.each((_, child) => { child.contentWindow.postMessage({ msg: 'appClosed', appInstanceID: instance_id, statusCode: status_code ?? 0, }, '*'); }); // TODO: Once other AppConnections exist, those will need notifying too. }; window.check_password_strength = (password) => { // Define criteria for password strength const criteria = { minLength: 8, hasUpperCase: /[A-Z]/.test(password), hasLowerCase: /[a-z]/.test(password), hasNumber: /\d/.test(password), hasSpecialChar: /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password) }; let overallPass = true; // Initialize report object let criteria_report = { minLength: { message: `Password must be at least ${criteria.minLength} characters long`, pass: password.length >= criteria.minLength, }, hasUpperCase: { message: 'Password must contain at least one uppercase letter', pass: criteria.hasUpperCase, }, hasLowerCase: { message: 'Password must contain at least one lowercase letter', pass: criteria.hasLowerCase, }, hasNumber: { message: 'Password must contain at least one number', pass: criteria.hasNumber, }, hasSpecialChar: { message: 'Password must contain at least one special character', pass: criteria.hasSpecialChar, }, }; // Check overall pass status and add messages for (let criterion in criteria) { if (!criteria_report[criterion].pass) { overallPass = false; break; } } return { overallPass: overallPass, report: criteria_report, }; }