/** * 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 UIWindowClaimReferral from "./UIWindowClaimReferral.js" import UIContextMenu from './UIContextMenu.js' import UIItem from './UIItem.js' import UIAlert from './UIAlert.js' import UIWindow from './UIWindow.js' import UIWindowSaveAccount from './UIWindowSaveAccount.js'; import UIWindowDesktopBGSettings from "./UIWindowDesktopBGSettings.js" import UIWindowMyWebsites from "./UIWindowMyWebsites.js" import UIWindowChangePassword from "./UIWindowChangePassword.js" import UIWindowChangeUsername from "./UIWindowChangeUsername.js" import UIWindowFeedback from "./UIWindowFeedback.js" import UIWindowLogin from "./UIWindowLogin.js" import UIWindowQR from "./UIWindowQR.js" import UIWindowRefer from "./UIWindowRefer.js" import UITaskbar from "./UITaskbar.js" import new_context_menu_item from "../helpers/new_context_menu_item.js" import ChangeLanguage from "../i18n/i18nChangeLanguage.js" async function UIDesktop(options){ let h = ''; // connect socket. window.socket = io(gui_origin + '/', { query: { auth_token: auth_token } }); window.socket.on('error', (error) => { console.error('GUI Socket Error:', error); }); window.socket.on('connect', function(){ console.log('GUI Socket: Connected', window.socket.id); }); window.socket.on('reconnect', function(){ console.log('GUI Socket: Reconnected', window.socket.id); }); window.socket.on('disconnect', () => { console.log('GUI Socket: Disconnected'); }); window.socket.on('reconnect', (attempt) => { console.log('GUI Socket: Reconnection', attempt); }); window.socket.on('reconnect_attempt', (attempt) => { console.log('GUI Socket: Reconnection Attemps', attempt); }); window.socket.on('reconnect_error', (error) => { console.log('GUI Socket: Reconnection Error', error); }); window.socket.on('reconnect_failed', () => { console.log('GUI Socket: Reconnection Failed'); }); window.socket.on('error', (error) => { console.error('GUI Socket Error:', error); }); socket.on('upload.progress', (msg) => { if(window.progress_tracker[msg.operation_id]){ window.progress_tracker[msg.operation_id].cloud_uploaded += msg.loaded_diff if(window.progress_tracker[msg.operation_id][msg.item_upload_id]){ window.progress_tracker[msg.operation_id][msg.item_upload_id].cloud_uploaded = msg.loaded; } } }); socket.on('download.progress', (msg) => { if(window.progress_tracker[msg.operation_id]){ if(window.progress_tracker[msg.operation_id][msg.item_upload_id]){ window.progress_tracker[msg.operation_id][msg.item_upload_id].downloaded = msg.loaded; window.progress_tracker[msg.operation_id][msg.item_upload_id].total = msg.total; } } }); socket.on('trash.is_empty', async (msg) => { $(`.item[data-path="${html_encode(trash_path)}" i]`).find('.item-icon > img').attr('src', msg.is_empty ? window.icons['trash.svg'] : window.icons['trash-full.svg']); $(`.window[data-path="${html_encode(trash_path)}" i]`).find('.window-head-icon').attr('src', msg.is_empty ? window.icons['trash.svg'] : window.icons['trash-full.svg']); // empty trash windows if needed if(msg.is_empty) $(`.window[data-path="${html_encode(trash_path)}" i]`).find('.item-container').empty(); }) socket.on('app.opened', async (app) => { // don't update if this is the original client that initiated the action if(app.original_client_socket_id === window.socket.id) return; // add the app to the beginning of the array launch_apps.recent.unshift(app); // dedupe the array by uuid, uid, and id launch_apps.recent = _.uniqBy(launch_apps.recent, 'name'); // limit to 5 launch_apps.recent = launch_apps.recent.slice(0, window.launch_recent_apps_count); }) socket.on('item.removed', async (item) => { // don't update if this is the original client that initiated the action if(item.original_client_socket_id === window.socket.id) return; // don't remove items if this was a descendants_only operation if(item.descendants_only) return; // hide all UIItems with matching uids $(`.item[data-path='${item.path}']`).fadeOut(150, function(){ // close all windows with matching uids // $('.window-' + item.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^="${item.path}/"]`).close(); }); }) socket.on('item.updated', async (item) => { // Don't update if this is the original client that initiated the action if(item.original_client_socket_id === window.socket.id) return; // Update matching items // set new item name $(`.item[data-uid='${html_encode(item.uid)}'] .item-name`).html(html_encode(truncate_filename(item.name, TRUNCATE_LENGTH)).replaceAll(' ', ' ')); // Set new icon const new_icon = (item.is_dir ? window.icons['folder.svg'] : (await item_icon(item)).image); $(`.item[data-uid='${item.uid}']`).find('.item-icon-thumb').attr('src', new_icon); $(`.item[data-uid='${item.uid}']`).find('.item-icon-icon').attr('src', new_icon); // Set new data-name $(`.item[data-uid='${item.uid}']`).attr('data-name', html_encode(item.name)); $(`.window-${item.uid}`).attr('data-name', html_encode(item.name)); // Set new title attribute $(`.item[data-uid='${item.uid}']`).attr('title', html_encode(item.name)); $(`.window-${options.uid}`).attr('title', html_encode(item.name)); // Set new value for item-name-editor $(`.item[data-uid='${item.uid}'] .item-name-editor`).val(html_encode(item.name)); $(`.item[data-uid='${item.uid}'] .item-name`).attr('title', html_encode(item.name)); // Set new data-path const new_path = item.path; $(`.item[data-uid='${item.uid}']`).attr('data-path', new_path); $(`.window-${item.uid}`).attr('data-path', new_path); // Update all elements that have matching paths $(`[data-path="${html_encode(item.old_path)}" i]`).each(function(){ $(this).attr('data-path', new_path) if($(this).hasClass('window-navbar-path-dirname')) $(this).text(item.name); }); // Update all elements whose paths start with old_path $(`[data-path^="${html_encode(item.old_path) + '/'}"]`).each(function(){ const new_el_path = _.replace($(this).attr('data-path'), item.old_path + '/', new_path+'/'); $(this).attr('data-path', new_el_path); }); // Update all exact-matching windows $(`.window-${item.uid}`).each(function(){ update_window_path(this, new_path); }) // Set new name for matching open windows $(`.window-${item.uid} .window-head-title`).text(item.name); // Re-sort all matching item containers $(`.item[data-uid='${item.uid}']`).parent('.item-container').each(function(){ sort_items(this, $(this).closest('.item-container').attr('data-sort_by'), $(this).closest('.item-container').attr('data-sort_order')); }) }) socket.on('item.moved', async (resp) => { let fsentry = resp; // Notify all apps that are watching this item sendItemChangeEventToWatchingApps(fsentry.uid, { event: 'moved', uid: fsentry.uid, name: fsentry.name, }) // don't update if this is the original client that initiated the action if(resp.original_client_socket_id === window.socket.id) return; let dest_path = path.dirname(fsentry.path); let metadata = fsentry.metadata; // path must use the real name from DB fsentry.path = fsentry.path; // update all shortcut_to_path $(`.item[data-shortcut_to_path="${html_encode(resp.old_path)}" i]`).attr(`data-shortcut_to_path`, html_encode(fsentry.path)); // remove all items with matching uids $(`.item[data-uid='${fsentry.uid}']`).fadeOut(150, function(){ // find all parent windows that contain this item let parent_windows = $(`.item[data-uid='${fsentry.uid}']`).closest('.window'); // remove this item $(this).removeItems(); // update parent windows' item counts $(parent_windows).each(function(index){ update_explorer_footer_item_count(this); update_explorer_footer_selected_items_count(this) }); }) // if trashing, close windows of trashed items and its descendants if(dest_path === trash_path){ $(`.window[data-path="${html_encode(resp.old_path)}" i]`).close(); // todo this has to be case-insensitive but the `i` selector doesn't work on ^= $(`.window[data-path^="${html_encode(resp.old_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(resp.old_path)}/"], .window[data-path="${html_encode(resp.old_path)}" i]`).each(function(){ update_window_path(this, $(this).attr('data-path').replace(resp.old_path, fsentry.path)); }) } if(dest_path === trash_path){ $(`.item[data-uid="${fsentry.uid}"]`).find('.item-is-shared').fadeOut(300); // if trashing dir... if(fsentry.is_dir){ // remove website badge $(`.mywebsites-dir-path[data-uuid="${fsentry.uid}"]`).remove(); // remove the website badge from all instances of the dir $(`.item[data-uid="${fsentry.uid}"]`).find('.item-has-website-badge').fadeOut(300); // remove File Rrequest Token // todo, some client-side check to see if this dir has an FR associated with it before sending a whole ajax req } } // if replacing an existing item, remove the old item that was just replaced if(fsentry.overwritten_uid !== undefined) $(`.item[data-uid=${fsentry.overwritten_uid}]`).removeItems(); // if this is trash, get original name from item metadata fsentry.name = (metadata && metadata.original_name) ? metadata.original_name : fsentry.name; // create new item on matching containers UIItem({ appendTo: $(`.item-container[data-path='${html_encode(dest_path)}' i]`), immutable: fsentry.immutable, uid: fsentry.uid, path: fsentry.path, icon: await item_icon(fsentry), name: (dest_path === trash_path) ? metadata.original_name : fsentry.name, is_dir: fsentry.is_dir, size: fsentry.size, type: fsentry.type, modified: fsentry.modified, is_selected: false, is_shared: (dest_path === 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: JSON.stringify(fsentry.metadata) ?? '', }); if(fsentry.parent_dirs_created && fsentry.parent_dirs_created.length > 0){ // 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 fsentry.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, }); } sort_items(item_container, $(item_container).attr('data-sort_by'), $(item_container).attr('data-sort_order')); }); } //sort each container $(`.item-container[data-path='${html_encode(dest_path)}' i]`).each(function(){ sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order')) }) }); socket.on('user.email_confirmed', (msg) => { // don't update if this is the original client that initiated the action if(msg.original_client_socket_id === window.socket.id) return; refresh_user_data(window.auth_token); }); socket.on('item.renamed', async (item) => { // Notify all apps that are watching this item sendItemChangeEventToWatchingApps(item.uid, { event: 'rename', uid: item.uid, // path: item.path, new_name: item.name, // old_path: item.old_path, }) // Don't update if this is the original client that initiated the action if(item.original_client_socket_id === window.socket.id) return; // Update matching items // Set new item name $(`.item[data-uid='${html_encode(item.uid)}'] .item-name`).html(html_encode(truncate_filename(item.name, TRUNCATE_LENGTH)).replaceAll(' ', ' ')); // Set new icon const new_icon = (item.is_dir ? window.icons['folder.svg'] : (await item_icon(item)).image); $(`.item[data-uid='${item.uid}']`).find('.item-icon-icon').attr('src', new_icon); // Set new data-name $(`.item[data-uid='${item.uid}']`).attr('data-name', html_encode(item.name)); $(`.window-${item.uid}`).attr('data-name', html_encode(item.name)); // Set new title attribute $(`.item[data-uid='${item.uid}']`).attr('title', html_encode(item.name)); $(`.window-${options.uid}`).attr('title', html_encode(item.name)); // Set new value for item-name-editor $(`.item[data-uid='${item.uid}'] .item-name-editor`).val(html_encode(item.name)); $(`.item[data-uid='${item.uid}'] .item-name`).attr('title', html_encode(item.name)); // Set new data-path const new_path = item.path; $(`.item[data-uid='${item.uid}']`).attr('data-path', new_path); $(`.window-${item.uid}`).attr('data-path', new_path); // Update all elements that have matching paths $(`[data-path="${html_encode(item.old_path)}" i]`).each(function(){ $(this).attr('data-path', new_path) if($(this).hasClass('window-navbar-path-dirname')) $(this).text(item.name); }); // Update all elements whose paths start with old_path $(`[data-path^="${html_encode(item.old_path) + '/'}"]`).each(function(){ const new_el_path = _.replace($(this).attr('data-path'), item.old_path + '/', new_path+'/'); $(this).attr('data-path', new_el_path); }); // Update all exact-matching windows $(`.window-${item.uid}`).each(function(){ update_window_path(this, new_path); }) // Set new name for matching open windows $(`.window-${item.uid} .window-head-title`).text(item.name); // Re-sort all matching item containers $(`.item[data-uid='${item.uid}']`).parent('.item-container').each(function(){ sort_items(this, $(this).closest('.item-container').attr('data-sort_by'), $(this).closest('.item-container').attr('data-sort_order')); }) }); socket.on('item.added', async (item) => { // if item is empty, don't proceed if(_.isEmpty(item)) return; // Notify all apps that are watching this item sendItemChangeEventToWatchingApps(item.uid, { event: 'write', uid: item.uid, // path: item.path, new_size: item.size, modified: item.modified, // old_path: item.old_path, }); // Don't update if this is the original client that initiated the action if(item.original_client_socket_id === window.socket.id) return; // Update replaced items with matching uids if(item.overwritten_uid){ $(`.item[data-uid='${item.overwritten_uid}']`).attr({ 'data-immutable': item.immutable, 'data-path': item.path, 'data-name': item.name, 'data-size': item.size, 'data-modified': item.modified, 'data-is_shared': item.is_shared, 'data-type': item.type, }) // set new icon const new_icon = (item.is_dir ? window.icons['folder.svg'] : (await item_icon(item)).image); $(`.item[data-uid="${item.overwritten_uid}"]`).find('.item-icon > img').attr('src', new_icon); //sort each window $(`.item-container[data-path='${html_encode(item.dirpath)}' i]`).each(function(){ sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order')) }) } else{ UIItem({ appendTo: $(`.item-container[data-path='${html_encode(item.dirpath)}' i]`), uid: item.uid, immutable: item.immutable, associated_app_name: item.associated_app?.name, path: item.path, icon: await item_icon(item), name: item.name, size: item.size, type: item.type, modified: item.modified, is_dir: item.is_dir, is_shared: item.is_shared, is_shortcut: item.is_shortcut, associated_app_name: item.associated_app?.name, shortcut_to: item.shortcut_to, shortcut_to_path: item.shortcut_to_path, }); //sort each window $(`.item-container[data-path='${html_encode(item.dirpath)}' i]`).each(function(){ sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order')) }) } }); // Hidden file dialog h += `
`; h += `
`; // Desktop // If desktop is not in fullpage/embedded mode, we hide it until files and directories are loaded and then fade in the UI // This gives a calm and smooth experience for the user h += `
`; h += `
`; // Get window sidebar width getItem({ key: "window_sidebar_width", success: async function(res){ let value = parseInt(res.value); // if value is a valid number if(!isNaN(value) && value > 0){ window.window_sidebar_width = value; } } }) // Remove `?ref=...` from navbar URL if(url_query_params.has('ref')){ window.history.pushState(null, document.title, '/'); } // update local user preferences const user_preferences = { show_hidden_files: (await puter.kv.get('user_preferences.show_hidden_files')) === 'true', }; update_user_preferences(user_preferences); // Append to $('body').append(h); // Set desktop height based on taskbar height $('.desktop').css('height', `calc(100vh - ${window.taskbar_height + window.toolbar_height}px)`) // --------------------------------------------------------------- // Taskbar // --------------------------------------------------------------- UITaskbar(); const el_desktop = document.querySelector('.desktop'); window.active_element = el_desktop; window.active_item_container = el_desktop; // -------------------------------------------------------- // Dragster // Allow dragging of local files onto desktop. // -------------------------------------------------------- $(el_desktop).dragster({ enter: function (dragsterEvent, event) { $('.context-menu').remove(); }, leave: function (dragsterEvent, event) { }, drop: async function (dragsterEvent, event) { const e = event.originalEvent; // no drop on item if($(event.target).hasClass('item') || $(event.target).parent('.item').length > 0) return false; // recursively create directories and upload files if(e.dataTransfer?.items?.length>0){ upload_items(e.dataTransfer.items, desktop_path); } e.stopPropagation(); e.preventDefault(); return false; } }); // -------------------------------------------------------- // Droppable // -------------------------------------------------------- $(el_desktop).droppable({ accept: '.item', tolerance: "intersect", drop: function( event, ui ) { // Check if item was actually dropped on desktop and not a window if(mouseover_window !== undefined) return; // Can't drop anything but UIItems on desktop if(!$(ui.draggable).hasClass('item')) return; // Don't move an item to its current directory if( path.dirname($(ui.draggable).attr('data-path')) === desktop_path && !event.ctrlKey) return; // If ctrl is pressed and source is Trashed, cancel whole operation if(event.ctrlKey && path.dirname($(ui.draggable).attr('data-path')) === window.trash_path) return; // Unselect previously selected items $(el_desktop).children('.item-selected').removeClass('item-selected'); const items_to_move = [] // first item items_to_move.push(ui.draggable); // all subsequent items const cloned_items = document.getElementsByClassName('item-selected-clone'); for(let i =0; i