/** * 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 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 truncate_filename from './helpers/truncate_filename.js'; import UIWindowProgress from './UI/UIWindowProgress.js'; import globToRegExp from "./helpers/globToRegExp.js"; import get_html_element_from_options from "./helpers/get_html_element_from_options.js"; import item_icon from "./helpers/item_icon.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); } } } /** * 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(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); } }); } } /** * 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){ puter.kv.get({ key: "save_account_notice_shown", }).then(async function(value){ if(!value && window.user?.is_temp){ puter.kv.set({ 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); }); }); } /** * 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 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 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); } } /** * 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){ // 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) { 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 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` attribute 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 get_html_element_from_options(item.options); window.move_items([el], path.dirname(item.original_path), true); } } window.undo_delete = async(items)=>{ for (const item of items) { const el = await 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.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.set_menu_item_prop = (items, item_id, prop, val) => { // iterate over items for (const item of items) { // find the item with the given item_id if (item.id === item_id) { // set the property value item[prop] = val; break; } else if(item.items){ set_menu_item_prop(item.items, item_id, prop, val); } } }; window.countSubstr = (str, substring)=>{ if (substring.length === 0) { return 0; } let count = 0; let pos = str.indexOf(substring); while (pos !== -1) { count++; pos = str.indexOf(substring, pos + 1); } return count; } window.detectHostOS = function(){ var userAgent = window.navigator.userAgent; var platform = window.navigator.platform; var macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']; var windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']; if (macosPlatforms.indexOf(platform) !== -1) { return 'macos'; } else if (windowsPlatforms.indexOf(platform) !== -1) { return 'windows'; } else { return 'other'; } }